diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index a814b8e4..3f2fe033 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -9,9 +9,6 @@ on: push: branches: - master - tags: - - 'v*' - - 'V*' permissions: contents: write @@ -37,7 +34,7 @@ jobs: fetch-depth: '0' - name: Setup .NET - uses: actions/setup-dotnet@v5.0.1 + uses: actions/setup-dotnet@v5.2.0 with: dotnet-version: '8.0.x' @@ -56,23 +53,6 @@ jobs: path: | ${{ github.workspace }}/v2rayN/Release/linux* - # release debian package - - name: Package debian - if: github.event.inputs.release_tag != '' - run: | - chmod 755 package-debian.sh - ./package-debian.sh "$OutputArch" "$OutputPath64" "${{ github.event.inputs.release_tag }}" - ./package-debian.sh "$OutputArchArm" "$OutputPathArm64" "${{ github.event.inputs.release_tag }}" - - - name: Upload deb to release - uses: svenstaro/upload-release-action@v2 - if: github.event.inputs.release_tag != '' - with: - file: ${{ github.workspace }}/v2rayN*.deb - tag: ${{ github.event.inputs.release_tag }} - file_glob: true - prerelease: true - # release zip archive - name: Package release zip archive if: github.event.inputs.release_tag != '' @@ -90,6 +70,65 @@ jobs: file_glob: true prerelease: true + deb: + needs: build + if: | + (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + runs-on: ubuntu-24.04 + container: + image: debian:13 + env: + RELEASE_TAG: ${{ github.event.inputs.release_tag != '' && github.event.inputs.release_tag || github.ref_name }} + + steps: + - name: Prepare tools (Debian) + shell: bash + run: | + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \ + ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \ + libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev + + - name: Checkout repo (for scripts) + uses: actions/checkout@v6.0.2 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Ensure script permissions + run: chmod 755 package-debian.sh + + - name: Package DEB (Debian-family) + run: ./package-debian.sh "${RELEASE_TAG}" --arch all + + - name: Collect DEBs into workspace + run: | + mkdir -p "$GITHUB_WORKSPACE/dist/deb" + rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true + find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \ + -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true + find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" \ + -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true + echo "==== Dist tree ====" + ls -R "$GITHUB_WORKSPACE/dist/deb" || true + + - name: Upload DEB artifacts + uses: actions/upload-artifact@v7.0.0 + with: + name: v2rayN-deb + path: dist/deb/**/*.deb + + - name: Upload DEBs to release + uses: svenstaro/upload-release-action@v2 + with: + file: dist/deb/**/*.deb + tag: ${{ env.RELEASE_TAG }} + file_glob: true + prerelease: true + rpm: needs: build if: | diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index f16f28bf..d79295cc 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -32,7 +32,7 @@ jobs: fetch-depth: '0' - name: Setup - uses: actions/setup-dotnet@v5.0.1 + uses: actions/setup-dotnet@v5.2.0 with: dotnet-version: '8.0.x' diff --git a/.github/workflows/build-windows-desktop.yml b/.github/workflows/build-windows-desktop.yml index 1a85e546..99c7ef32 100644 --- a/.github/workflows/build-windows-desktop.yml +++ b/.github/workflows/build-windows-desktop.yml @@ -32,7 +32,7 @@ jobs: fetch-depth: '0' - name: Setup - uses: actions/setup-dotnet@v5.0.1 + uses: actions/setup-dotnet@v5.2.0 with: dotnet-version: '8.0.x' diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 4c1ce3ea..7d202370 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v6.0.2 - name: Setup - uses: actions/setup-dotnet@v5.0.1 + uses: actions/setup-dotnet@v5.2.0 with: dotnet-version: '8.0.x' diff --git a/package-debian.sh b/package-debian.sh index 3b91e7ca..fa21461d 100644 --- a/package-debian.sh +++ b/package-debian.sh @@ -1,70 +1,607 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -Arch="$1" -OutputPath="$2" -Version="$3" +# Require Debian base branch +. /etc/os-release -FileName="v2rayN-${Arch}.zip" -wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/$FileName" -7z x $FileName -cp -rf v2rayN-${Arch}/* $OutputPath +case "${ID:-}" in + debian) + echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}" + ;; + *) + echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})." + echo "This script only supports: Debian." + exit 1 + ;; +esac -PackagePath="v2rayN-Package-${Arch}" -mkdir -p "${PackagePath}/DEBIAN" -mkdir -p "${PackagePath}/opt" -cp -rf $OutputPath "${PackagePath}/opt/v2rayN" -echo "When this file exists, app will not store configs under this folder" > "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt" +# Kernel version +MIN_KERNEL="6.11" +CURRENT_KERNEL="$(uname -r)" -if [ $Arch = "linux-64" ]; then - Arch2="amd64" -else - Arch2="arm64" +lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$CURRENT_KERNEL" | sort -V | head -n1)" + +if [[ "$lowest" != "$MIN_KERNEL" ]]; then + echo "Kernel $CURRENT_KERNEL is below $MIN_KERNEL" + exit 1 fi -echo $Arch2 -# basic -cat >"${PackagePath}/DEBIAN/control" <<-EOF -Package: v2rayN -Version: $Version -Architecture: $Arch2 -Maintainer: https://github.com/2dust/v2rayN -Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11) -Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others +echo "[OK] Kernel $CURRENT_KERNEL verified." + +# 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 +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;; + --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 "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 + +# Check and install dependencies +host_arch="$(uname -m)" +[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; } + +install_ok=0 + +if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get -y install \ + curl unzip tar jq rsync ca-certificates git dpkg-dev fakeroot file \ + desktop-file-utils xdg-utils wget + + if [[ "$host_arch" == "aarch64" ]]; then + sudo dpkg --add-architecture amd64 || true + sudo apt-get update + sudo apt-get -y install \ + libc6:amd64 libgcc-s1:amd64 libstdc++6:amd64 zlib1g:amd64 libfontconfig1:amd64 + elif [[ "$host_arch" == "x86_64" ]]; then + sudo dpkg --add-architecture arm64 || true + sudo apt-get update + sudo apt-get -y install \ + libc6:arm64 libgcc-s1:arm64 libstdc++6:arm64 zlib1g:arm64 libfontconfig1:arm64 + fi + + # Install .NET SDK 8 via official script + wget -q https://dot.net/v1/dotnet-install.sh + chmod +x dotnet-install.sh + ./dotnet-install.sh --channel 8.0 --install-dir "$HOME/.dotnet" + + export PATH="$HOME/.dotnet:$PATH" + export DOTNET_ROOT="$HOME/.dotnet" + + dotnet --info >/dev/null 2>&1 && install_ok=1 +fi + +if [[ "$install_ok" -ne 1 ]]; then + echo "Could not auto-install dependencies for '$ID'. Make sure these are available:" + echo "dotnet-sdk 8.x, curl, unzip, tar, rsync, git, dpkg-deb, desktop-file-utils, xdg-utils" + exit 1 +fi + +# Root directory +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; } + +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 + git fetch --tags --force --prune --depth=1 || true + if git rev-parse "refs/tags/${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 +} + +apply_channel_or_keep() { + local ch="$1" tag + + if [[ "$ch" == "keep" ]]; then + echo "[*] Keep current repository state (no checkout)." + VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0+git')" + VERSION="${VERSION#v}" + return 0 + fi + + echo "[*] Resolving ${ch} tag from GitHub releases..." + if [[ "$ch" == "prerelease" ]]; then + tag="$(get_latest_tag_prerelease || true)" + else + tag="$(get_latest_tag_latest || true)" + fi + + [[ -n "$tag" ]] || { echo "Failed to resolve latest tag for channel '${ch}'."; exit 1; } + echo "[*] Latest tag for '${ch}': ${tag}" + git_try_checkout "$tag" || { echo "Failed to checkout '${tag}'."; exit 1; } + VERSION="${tag#v}" +} + +if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + clean_ver="${VERSION_ARG#v}" + if git_try_checkout "$clean_ver"; then + VERSION="$clean_ver" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + ch="$(choose_channel)" + apply_channel_or_keep "$ch" + fi + else + ch="$(choose_channel)" + apply_channel_or_keep "$ch" + fi +else + echo "Current directory is not a git repo; proceeding on current tree." + VERSION="${VERSION_ARG:-0.0.0}" +fi + +VERSION="${VERSION#v}" +echo "[*] GUI version resolved as: ${VERSION}" + +download_xray() { + local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" + mkdir -p "$outdir" + if [[ -z "$ver" ]]; then + ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \ + | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true + fi + [[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; } + if [[ "$rid" == "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)" + curl -fL "$url" -o "$tmp/$zipname" + unzip -q "$tmp/$zipname" -d "$tmp" + install -m 755 "$tmp/xray" "$outdir/xray" + rm -rf "$tmp" +} + +download_singbox() { + local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin + mkdir -p "$outdir" + if [[ -z "$ver" ]]; then + ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true + fi + [[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; } + if [[ "$rid" == "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)" + 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"; rm -rf "$tmp"; return 1; } + install -m 755 "$bin" "$outdir/sing-box" + rm -rf "$tmp" +} + +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 [[ -f "$outroot/bin/xray/$n" ]]; then + mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n" + fi + done +} + +download_geo_assets() { + local outroot="$1" + local bin_dir="$outroot/bin" + local srss_dir="$bin_dir/srss" + 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-google.srs geosite-greatfire.srs \ + geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do + curl -fsSL -o "$srss_dir/$f" \ + "https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true + done + + unify_geo_layout "$outroot" +} + +download_v2rayn_bundle() { + local outroot="$1" rid="$2" + local url="" + if [[ "$rid" == "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 + 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" +} + +BUILT_DEBS=() +BUILT_ALL=0 +OUTPUT_DIR="$HOME/debbuild" +mkdir -p "$OUTPUT_DIR" + +build_for_arch() { + local short="$1" + local rid deb_arch outdir_name + case "$short" in + x64) rid="linux-x64"; deb_arch="amd64"; outdir_name="amd64" ;; + arm64) rid="linux-arm64"; deb_arch="arm64"; outdir_name="arm64" ;; + *) echo "Unknown arch '$short' (use x64|arm64)"; return 1 ;; + esac + + echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)" + + 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 + + local RID_DIR="$rid" + local PUBDIR + PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish" + [[ -d "$PUBDIR" ]] || { echo "Publish directory not found: $PUBDIR"; return 1; } + + local WORKDIR PKGROOT STAGE DEBIAN_DIR + WORKDIR="$(mktemp -d)" + PKGROOT="v2rayN-publish" + STAGE="$WORKDIR/${PKGROOT}_${VERSION}_${deb_arch}" + DEBIAN_DIR="$STAGE/DEBIAN" + + mkdir -p "$STAGE/opt/v2rayN" + mkdir -p "$STAGE/usr/bin" + mkdir -p "$STAGE/usr/share/applications" + mkdir -p "$STAGE/usr/share/icons/hicolor/256x256/apps" + mkdir -p "$DEBIAN_DIR" + + # Stage publish content from source build + cp -a "$PUBDIR/." "$STAGE/opt/v2rayN/" + + local ICON_CANDIDATE + PROJECT_DIR="$(cd "$(dirname "$PROJECT")" && pwd)" + ICON_CANDIDATE="$PROJECT_DIR/v2rayN.png" + [[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$STAGE/usr/share/icons/hicolor/256x256/apps/v2rayn.png" || true + + mkdir -p "$STAGE/opt/v2rayN/bin/xray" "$STAGE/opt/v2rayN/bin/sing_box" + + fetch_separate_cores_and_rules() { + local outroot="$1" + + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$RID_DIR" || echo "[!] xray download failed (skipped)" + fi + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$outroot/bin/sing_box" "$RID_DIR" || echo "[!] sing-box download failed (skipped)" + fi + download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" + } + + if [[ "$FORCE_NETCORE" -eq 0 ]]; then + if download_v2rayn_bundle "$STAGE/opt/v2rayN" "$RID_DIR"; then + echo "[*] Using v2rayN bundle bin assets." + else + echo "[*] Bundle failed, fallback to separate core + rules." + fetch_separate_cores_and_rules "$STAGE/opt/v2rayN" + fi + else + echo "[*] --netcore specified: use separate core + rules." + fetch_separate_cores_and_rules "$STAGE/opt/v2rayN" + fi + + # Wrapper + install -m 755 /dev/stdin "$STAGE/usr/bin/v2rayn" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +DIR="/opt/v2rayN" +cd "$DIR" + +if [[ -x "$DIR/v2rayN" ]]; then + exec "$DIR/v2rayN" "$@" +fi + +for dll in v2rayN.Desktop.dll v2rayN.dll; do + if [[ -f "$DIR/$dll" ]]; then + exec /usr/bin/dotnet "$DIR/$dll" "$@" + fi +done + +echo "v2rayN launcher: no executable found in $DIR" >&2 +ls -l "$DIR" >&2 || true +exit 1 EOF -mkdir -p "${PackagePath}/usr/share/applications" -cat >"${PackagePath}/usr/share/applications/v2rayN.desktop" <<-EOF + SHLIBS_DEPENDS="" + EXTRA_DEPENDS="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)" + + mkdir -p "$WORKDIR/debian" + cat > "$WORKDIR/debian/control" < +Standards-Version: 4.7.0 + +Package: v2rayn +Architecture: ${deb_arch} +Description: v2rayN +EOF + + local SYS_LIBDIR="" + local SYS_USRLIBDIR="" + multiarch="$(dpkg-architecture -a"$deb_arch" -qDEB_HOST_MULTIARCH)" + + SYS_LIBDIR="/lib/$multiarch" + SYS_USRLIBDIR="/usr/lib/$multiarch" + + : > "$DEBIAN_DIR/substvars" + mapfile -t ELF_FILES < <( + find "$STAGE/opt/v2rayN" -type f \( -name "*.so*" -o -perm -111 \) ! -name 'libcoreclrtraceptprovider.so' + ) + if [[ "${#ELF_FILES[@]}" -gt 0 ]]; then + ( + cd "$WORKDIR" + dpkg-shlibdeps \ + -l"$STAGE/opt/v2rayN" \ + -l"$SYS_LIBDIR" \ + -l"$SYS_USRLIBDIR" \ + -T"$DEBIAN_DIR/substvars" \ + "${ELF_FILES[@]}" + ) >/dev/null 2>&1 || true + fi + + SHLIBS_DEPENDS="$(sed -n 's/^shlibs:Depends=//p' "$DEBIAN_DIR/substvars" | head -n1 || true)" + + if [[ -n "$SHLIBS_DEPENDS" ]]; then + SHLIBS_DEPENDS="$(echo "$SHLIBS_DEPENDS" \ + | sed -E 's/ *\([^)]*\)//g' \ + | sed -E 's/ *, */, /g' \ + | sed -E 's/^, *//; s/, *$//')" + FINAL_DEPENDS="${SHLIBS_DEPENDS}, ${EXTRA_DEPENDS}" + else + FINAL_DEPENDS="${EXTRA_DEPENDS}" + fi + + # Desktop file + install -m 644 /dev/stdin "$STAGE/usr/share/applications/v2rayn.desktop" <<'EOF' [Desktop Entry] -Name=v2rayN -Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others -Exec=/opt/v2rayN/v2rayN -Icon=/opt/v2rayN/v2rayN.png -Terminal=false Type=Application -Categories=Network;Application; +Name=v2rayN +Comment=v2rayN for Debian GNU Linux +Exec=v2rayn +Icon=v2rayn +Terminal=false +Categories=Network; EOF -cat >"${PackagePath}/DEBIAN/postinst" <<-'EOF' + # Control file + cat > "$DEBIAN_DIR/control" < +Homepage: https://github.com/2dust/v2rayN +Section: net +Priority: optional +Depends: ${FINAL_DEPENDS} +Description: v2rayN (Avalonia) GUI client for Linux + Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / + Shadowsocks / tuic / WireGuard. +EOF + + # postinst + install -m 755 /dev/stdin "$DEBIAN_DIR/postinst" <<'EOF' +#!/bin/sh set -e -update-desktop-database || true +update-desktop-database /usr/share/applications >/dev/null 2>&1 || true +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true +fi exit 0 EOF -sudo chmod 0755 "${PackagePath}/DEBIAN/postinst" -sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN" -sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool" + # postrm + install -m 755 /dev/stdin "$DEBIAN_DIR/postrm" <<'EOF' +#!/bin/sh +set -e +update-desktop-database /usr/share/applications >/dev/null 2>&1 || true +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true +fi +exit 0 +EOF -# Patch -# set owner to root:root -sudo chown -R root:root "${PackagePath}" -# set all directories to 755 (readable & traversable by all users) -sudo find "${PackagePath}/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 + # Normalize permissions + find "$STAGE/opt/v2rayN" -type d -exec chmod 0755 {} + + find "$STAGE/opt/v2rayN" -type f -exec chmod 0644 {} + + [[ -f "$STAGE/opt/v2rayN/v2rayN" ]] && chmod 0755 "$STAGE/opt/v2rayN/v2rayN" || true + + local deb_out + deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb" -# build deb package -sudo dpkg-deb -Zxz --build $PackagePath -sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb" + dpkg-deb --root-owner-group --build "$STAGE" "$deb_out" + + echo "Build done for $short. DEB at:" + echo " $deb_out" + BUILT_DEBS+=("$deb_out") + + rm -rf "$WORKDIR" +} + +case "${ARCH_OVERRIDE:-}" in + all) targets=(x64 arm64); BUILT_ALL=1 ;; + x64|amd64) targets=(x64) ;; + arm64|aarch64) targets=(arm64) ;; + "") targets=($([[ "$host_arch" == "aarch64" ]] && echo arm64 || echo x64)) ;; + *) echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."; exit 1 ;; +esac + +for arch in "${targets[@]}"; do + build_for_arch "$arch" +done + +echo "" +echo "================ Build Summary =================" +if [[ "${#BUILT_DEBS[@]}" -gt 0 ]]; then + echo "Output directory: $OUTPUT_DIR" + for pkg in "${BUILT_DEBS[@]}"; do + echo "$pkg" + done +else + echo "No DEBs detected in summary (check build logs above)." +fi +echo "===============================================" diff --git a/package-rhel.sh b/package-rhel.sh index 1843c850..bf295134 100644 --- a/package-rhel.sh +++ b/package-rhel.sh @@ -210,46 +210,48 @@ echo "[*] GUI version resolved as: ${VERSION}" # Helpers for core download_xray() { # Download Xray core - local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" + local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" mkdir -p "$outdir" if [[ -z "$ver" ]]; then ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \ | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true fi [[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; } - if [[ "$RID_DIR" == "linux-arm64" ]]; then + if [[ "$rid" == "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 + tmp="$(mktemp -d)" curl -fL "$url" -o "$tmp/$zipname" unzip -q "$tmp/$zipname" -d "$tmp" - install -Dm755 "$tmp/xray" "$outdir/xray" + install -m 755 "$tmp/xray" "$outdir/xray" + rm -rf "$tmp" } download_singbox() { # Download sing-box - local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin + local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin mkdir -p "$outdir" if [[ -z "$ver" ]]; then ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \ | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true fi [[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; } - if [[ "$RID_DIR" == "linux-arm64" ]]; then + if [[ "$rid" == "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 + tmp="$(mktemp -d)" 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" + [[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; rm -rf "$tmp"; return 1; } + install -m 755 "$bin" "$outdir/sing-box" + rm -rf "$tmp" } # Move geo files to outroot/bin @@ -310,9 +312,9 @@ download_geo_assets() { # Prefer the prebuilt v2rayN core bundle; then unify geo layout download_v2rayn_bundle() { - local outroot="$1" + local outroot="$1" rid="$2" local url="" - if [[ "$RID_DIR" == "linux-arm64" ]]; then + if [[ "$rid" == "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" @@ -378,10 +380,7 @@ build_for_arch() { 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 + [[ -d "$PUBDIR" ]] || { echo "Publish directory not found: $PUBDIR"; return 1; } # Per-arch working area local PKGROOT="v2rayN-publish" @@ -400,10 +399,12 @@ build_for_arch() { mkdir -p "$WORKDIR/$PKGROOT" cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/" - # Optional icon + # Required icon local ICON_CANDIDATE - ICON_CANDIDATE="$(dirname "$PROJECT")/../v2rayN.Desktop/v2rayN.png" - [[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png" || true + PROJECT_DIR="$(cd "$(dirname "$PROJECT")" && pwd)" + ICON_CANDIDATE="$PROJECT_DIR/v2rayN.png" + [[ -f "$ICON_CANDIDATE" ]] || { echo "Required icon not found: $ICON_CANDIDATE"; return 1; } + cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png" # Prepare bin structure mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box" @@ -413,16 +414,16 @@ build_for_arch() { local outroot="$1" if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then - download_xray "$outroot/bin/xray" || echo "[!] xray download failed (skipped)" + download_xray "$outroot/bin/xray" "$RID_DIR" || echo "[!] xray download failed (skipped)" fi if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then - download_singbox "$outroot/bin/sing_box" || echo "[!] sing-box download failed (skipped)" + download_singbox "$outroot/bin/sing_box" "$RID_DIR" || echo "[!] sing-box download failed (skipped)" fi download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" } if [[ "$FORCE_NETCORE" -eq 0 ]]; then - if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then + if download_v2rayn_bundle "$WORKDIR/$PKGROOT" "$RID_DIR"; then echo "[*] Using v2rayN bundle archive." else echo "[*] Bundle failed, fallback to separate core + rules." @@ -484,9 +485,14 @@ https://github.com/2dust/v2rayN install -dm0755 %{buildroot}/opt/v2rayN cp -a * %{buildroot}/opt/v2rayN/ +# Normalize permissions +find %{buildroot}/opt/v2rayN -type d -exec chmod 0755 {} + +find %{buildroot}/opt/v2rayN -type f -exec chmod 0644 {} + +[ -f %{buildroot}/opt/v2rayN/v2rayN ] && chmod 0755 %{buildroot}/opt/v2rayN/v2rayN || : + # Launcher (prefer native ELF first, then DLL fallback) install -dm0755 %{buildroot}%{_bindir} -cat > %{buildroot}%{_bindir}/v2rayn << 'EOF' +install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF' #!/usr/bin/bash set -euo pipefail DIR="/opt/v2rayN" @@ -503,11 +509,10 @@ 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' +install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF' [Desktop Entry] Type=Application Name=v2rayN @@ -519,10 +524,8 @@ 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 +install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps +install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png %post /usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index e7dffdd2..b6052325 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,7 +1,7 @@ - 7.19.1 + 7.19.5 diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 0921f480..e1c29791 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -9,18 +9,18 @@ - + - + - + - + - + - + diff --git a/v2rayN/ServiceLib/Enums/EConfigType.cs b/v2rayN/ServiceLib/Enums/EConfigType.cs index 287d7b94..ae4e30ca 100644 --- a/v2rayN/ServiceLib/Enums/EConfigType.cs +++ b/v2rayN/ServiceLib/Enums/EConfigType.cs @@ -13,6 +13,7 @@ public enum EConfigType WireGuard = 9, HTTP = 10, Anytls = 11, + Naive = 12, PolicyGroup = 101, ProxyChain = 102, } diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 88a97398..acbf85b9 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -95,7 +95,7 @@ public class Global public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$"; - public static readonly List PolicyGroupDefaultFilterList = + public static readonly List PolicyGroupDefaultFilterList = [ // All nodes (exclude traffic/expiry info) PolicyGroupDefaultAllFilter, @@ -193,18 +193,23 @@ public class Global public const string Hysteria2ProtocolShare = "hy2://"; + public const string NaiveHttpsProtocolShare = "naive+https://"; + + public const string NaiveQuicProtocolShare = "naive+quic://"; + public static readonly Dictionary ProtocolShares = new() { - { EConfigType.VMess, "vmess://" }, - { EConfigType.Shadowsocks, "ss://" }, - { EConfigType.SOCKS, "socks://" }, - { EConfigType.HTTP, "http://" }, - { EConfigType.VLESS, "vless://" }, - { EConfigType.Trojan, "trojan://" }, - { EConfigType.Hysteria2, "hysteria2://" }, - { EConfigType.TUIC, "tuic://" }, - { EConfigType.WireGuard, "wireguard://" }, - { EConfigType.Anytls, "anytls://" } + { EConfigType.VMess, "vmess://" }, + { EConfigType.Shadowsocks, "ss://" }, + { EConfigType.SOCKS, "socks://" }, + { EConfigType.HTTP, "http://" }, + { EConfigType.VLESS, "vless://" }, + { EConfigType.Trojan, "trojan://" }, + { EConfigType.Hysteria2, "hysteria2://" }, + { EConfigType.TUIC, "tuic://" }, + { EConfigType.WireGuard, "wireguard://" }, + { EConfigType.Anytls, "anytls://" }, + { EConfigType.Naive, "naive://" } }; public static readonly Dictionary ProtocolTypes = new() @@ -218,7 +223,8 @@ public class Global { EConfigType.Hysteria2, "hysteria2" }, { EConfigType.TUIC, "tuic" }, { EConfigType.WireGuard, "wireguard" }, - { EConfigType.Anytls, "anytls" } + { EConfigType.Anytls, "anytls" }, + { EConfigType.Naive, "naive" } }; public static readonly List VmessSecurities = @@ -343,6 +349,7 @@ public class Global EConfigType.Hysteria2, EConfigType.TUIC, EConfigType.Anytls, + EConfigType.Naive, EConfigType.WireGuard, EConfigType.SOCKS, EConfigType.HTTP, @@ -559,6 +566,14 @@ public class Global "bbr" ]; + public static readonly List NaiveCongestionControls = + [ + "bbr", + "bbr2", + "cubic", + "reno" + ]; + public static readonly List allowSelectType = [ "selector", @@ -661,5 +676,14 @@ public class Global "" ]; + public static readonly List TunIcmpRoutingPolicies = + [ + "rule", + "direct", + "unreachable", + "drop", + "reply", + ]; + #endregion const } diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index 4894b334..a8cca654 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -227,7 +227,7 @@ public class CoreConfigContextBuilder { var result = NodeValidatorResult.Empty(); - if (node.Subid.IsNullOrEmpty()) + if (node.Subid.IsNullOrEmpty() || node.ConfigType == EConfigType.Custom) { return (null, result); } @@ -268,7 +268,8 @@ public class CoreConfigContextBuilder { IndexId = $"inner-{Utils.GetGuid(false)}", ConfigType = EConfigType.ProxyChain, - CoreType = node.CoreType ?? ECoreType.Xray, + CoreType = AppManager.Instance.GetCoreType(node, node.ConfigType), + Remarks = node.Remarks, }; List childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId]; var chainExtraItem = chainNode.GetProtocolExtra() with @@ -321,6 +322,7 @@ public class CoreConfigContextBuilder context.ProtectDomainList.Add(address); } + // ech query server name protect if (!node.EchConfigList.IsNullOrEmpty()) { var echQuerySni = node.Sni; @@ -337,6 +339,20 @@ public class CoreConfigContextBuilder } } + // xhttp downloadSettings address protect + if (!string.IsNullOrEmpty(node.Extra) + && JsonUtils.ParseJson(node.Extra) is JsonObject extra + && extra.TryGetPropertyValue("downloadSettings", out var dsNode) + && dsNode is JsonObject downloadSettings + && downloadSettings.TryGetPropertyValue("address", out var dAddrNode) + && dAddrNode is JsonValue dAddrValue + && dAddrValue.TryGetValue(out string? dAddr) + && !string.IsNullOrEmpty(dAddr) + && Utils.IsDomain(dAddr)) + { + context.ProtectDomainList.Add(dAddr); + } + return nodeValidatorResult; } diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs index a6adf1c2..65110b2a 100644 --- a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -81,7 +81,9 @@ public class NodeValidator { var transportError = ValidateSingboxTransport(item.ConfigType, net); if (transportError != null) + { v.Error(transportError); + } if (!Global.SingboxSupportConfigType.Contains(item.ConfigType)) { diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index f027761a..88b1d2a5 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -91,14 +91,12 @@ public static class ConfigHandler { EnableTun = false, Mtu = 9000, + IcmpRouting = Global.TunIcmpRoutingPolicies.First(), }; config.GuiItem ??= new(); config.MsgUIItem ??= new(); - config.UiItem ??= new UIItem() - { - EnableUpdateSubOnlyRemarksExist = true - }; + config.UiItem ??= new(); config.UiItem.MainColumnItem ??= new(); config.UiItem.WindowSizeItem ??= new(); @@ -154,6 +152,7 @@ public static class ConfigHandler DownMbps = 100 }; config.ClashUIItem ??= new(); + config.ClashUIItem.ConnectionsColumnItem ??= new(); config.SystemProxyItem ??= new(); config.WebDavItem ??= new(); config.CheckUpdateItem ??= new(); @@ -271,6 +270,7 @@ public static class ConfigHandler EConfigType.TUIC => await AddTuicServer(config, item), EConfigType.WireGuard => await AddWireguardServer(config, item), EConfigType.Anytls => await AddAnytlsServer(config, item), + EConfigType.Naive => await AddNaiveServer(config, item), _ => -1, }; return ret; @@ -722,8 +722,6 @@ public static class ConfigHandler profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with { SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(), - UpMbps = profileItem.GetProtocolExtra().UpMbps is null or < 0 ? config.HysteriaItem.UpMbps : profileItem.GetProtocolExtra().UpMbps, - DownMbps = profileItem.GetProtocolExtra().DownMbps is null or < 0 ? config.HysteriaItem.DownMbps : profileItem.GetProtocolExtra().DownMbps, HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(), }); @@ -808,7 +806,7 @@ public static class ConfigHandler } /// - /// Add or edit a Anytls server + /// Add or edit an Anytls server /// Validates and processes Anytls-specific settings /// /// Current configuration @@ -835,6 +833,36 @@ public static class ConfigHandler return 0; } + /// + /// Add or edit a Naive server + /// Validates and processes Naive-specific settings + /// + /// Current configuration + /// Naive profile to add + /// Whether to save to file + /// 0 if successful, -1 if failed + public static async Task AddNaiveServer(Config config, ProfileItem profileItem, bool toFile = true) + { + profileItem.ConfigType = EConfigType.Naive; + profileItem.CoreType = ECoreType.sing_box; + + profileItem.Address = profileItem.Address.TrimEx(); + profileItem.Username = profileItem.Username.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); + profileItem.Alpn = string.Empty; + profileItem.Network = string.Empty; + if (profileItem.StreamSecurity.IsNullOrEmpty()) + { + profileItem.StreamSecurity = Global.StreamSecurity; + } + if (profileItem.Password.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 @@ -1041,8 +1069,8 @@ public static class ConfigHandler if (profileItem.StreamSecurity.IsNotEmpty()) { - if (profileItem.StreamSecurity != Global.StreamSecurity - && profileItem.StreamSecurity != Global.StreamSecurityReality) + if (profileItem.StreamSecurity is not Global.StreamSecurity + and not Global.StreamSecurityReality) { profileItem.StreamSecurity = string.Empty; } @@ -1081,7 +1109,8 @@ public static class ConfigHandler if (toFile) { - profileItem.SetProtocolExtra(); + //profileItem.SetProtocolExtra(); + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra()); await SQLiteHelper.Instance.ReplaceAsync(profileItem); } return 0; @@ -1109,6 +1138,7 @@ public static class ConfigHandler && AreEqual(o.Address, n.Address) && o.Port == n.Port && AreEqual(o.Password, n.Password) + && AreEqual(o.Username, n.Username) && AreEqual(oProtocolExtra.VlessEncryption, nProtocolExtra.VlessEncryption) && AreEqual(oProtocolExtra.SsMethod, nProtocolExtra.SsMethod) && AreEqual(oProtocolExtra.VmessSecurity, nProtocolExtra.VmessSecurity) @@ -1133,6 +1163,84 @@ public static class ConfigHandler } } + /// + /// Searches the specified collection for a profile item that matches the target profile item based on a series of + /// criteria. + /// + /// The method attempts to find a match by comparing the target's remarks, address, port, and + /// password in various combinations. The search is performed in order of specificity, starting with the most + /// detailed comparison. If no match is found at any stage, the method returns null. + /// An enumerable collection of profile items to search. This parameter can be null. + /// The profile item to match against items in the source collection. This parameter can be null. + /// A profile item from the source collection that matches the target item according to defined criteria; otherwise, + /// null if no match is found or if either parameter is null. + private static ProfileItem? FindMatchedProfileItem(IEnumerable? source, ProfileItem? target) + { + if (source == null || target == null) + { + return null; + } + + var matchedItem = source.FirstOrDefault(t => CompareProfileItem(t, target, true)); + if (matchedItem != null) + { + return matchedItem; + } + + if (target.Remarks.IsNotEmpty()) + { + matchedItem = source.FirstOrDefault(t => t.Remarks == target.Remarks); + if (matchedItem != null) + { + return matchedItem; + } + } + + if (target.Address.IsNotEmpty() && target.Port > 0 && target.Password.IsNotEmpty()) + { + matchedItem = source.FirstOrDefault(t => + IsSameText(t.Address, target.Address) && + t.Port == target.Port && + IsSameText(t.Password, target.Password)); + if (matchedItem != null) + { + return matchedItem; + } + } + + if (target.Address.IsNotEmpty() && target.Port > 0) + { + matchedItem = source.FirstOrDefault(t => + IsSameText(t.Address, target.Address) && + t.Port == target.Port); + if (matchedItem != null) + { + return matchedItem; + } + } + + if (target.Address.IsNotEmpty()) + { + matchedItem = source.FirstOrDefault(t => IsSameText(t.Address, target.Address)); + if (matchedItem != null) + { + return matchedItem; + } + } + + return null; + + static bool IsSameText(string? left, string? right) + { + if (left.IsNullOrEmpty() || right.IsNullOrEmpty()) + { + return false; + } + + return string.Equals(left.TrimEx(), right.TrimEx(), StringComparison.OrdinalIgnoreCase); + } + } + /// /// Remove a single server profile by its index ID /// Deletes the configuration file if it's a custom config @@ -1332,6 +1440,7 @@ public static class ConfigHandler public static async Task RemoveInvalidServerResult(Config config, string subid) { var lstModel = await AppManager.Instance.ProfileModels(subid, ""); + lstModel.RemoveAll(t => t.ConfigType.IsComplexType()); if (lstModel is { Count: <= 0 }) { return -1; @@ -1413,6 +1522,7 @@ public static class ConfigHandler EConfigType.TUIC => await AddTuicServer(config, profileItem, false), EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false), EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false), + EConfigType.Naive => await AddNaiveServer(config, profileItem, false), _ => -1, }; @@ -1628,7 +1738,7 @@ public static class ConfigHandler if (activeProfile != null) { var lstSub = await AppManager.Instance.ProfileItems(subid); - var existItem = lstSub?.FirstOrDefault(t => config.UiItem.EnableUpdateSubOnlyRemarksExist ? t.Remarks == activeProfile.Remarks : CompareProfileItem(t, activeProfile, true)); + var existItem = FindMatchedProfileItem(lstSub, activeProfile); if (existItem != null) { await ConfigHandler.SetDefaultServerIndex(config, existItem.IndexId); @@ -1641,7 +1751,7 @@ public static class ConfigHandler 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)); + var existItem = FindMatchedProfileItem(lstOriSub, item); if (existItem != null) { await StatisticsManager.Instance.CloneServerStatItem(existItem.IndexId, item.IndexId); diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 474234e4..99f65662 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -21,7 +21,7 @@ public static class CoreConfigHandler _ => await GenerateClientCustomConfig(node, fileName) }; } - else if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box) + else if (context.RunCoreType == ECoreType.sing_box) { result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); } @@ -128,12 +128,11 @@ public static class CoreConfigHandler public static async Task GenerateClientSpeedtestConfig(Config config, CoreConfigContext context, ServerTestItem testItem, string fileName) { var result = new RetResult(); - var node = context.Node; var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest); var port = Utils.GetFreePort(initPort + testItem.QueueNum); testItem.Port = port; - if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box) + if (context.RunCoreType == ECoreType.sing_box) { result = new CoreConfigSingboxService(context).GenerateClientSpeedtestConfig(port); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs index e83d7087..cdc55565 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs @@ -20,6 +20,7 @@ public class FmtHandler EConfigType.TUIC => TuicFmt.ToUri(item), EConfigType.WireGuard => WireguardFmt.ToUri(item), EConfigType.Anytls => AnytlsFmt.ToUri(item), + EConfigType.Naive => NaiveFmt.ToUri(item), _ => null, }; @@ -85,6 +86,12 @@ public class FmtHandler { return AnytlsFmt.Resolve(str, out msg); } + else if (str.StartsWith(Global.ProtocolShares[EConfigType.Naive]) + || str.StartsWith(Global.NaiveHttpsProtocolShare) + || str.StartsWith(Global.NaiveQuicProtocolShare)) + { + return NaiveFmt.Resolve(str, out msg); + } else { msg = ResUI.NonvmessOrssProtocol; diff --git a/v2rayN/ServiceLib/Handler/Fmt/NaiveFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/NaiveFmt.cs new file mode 100644 index 00000000..5792472b --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Fmt/NaiveFmt.cs @@ -0,0 +1,91 @@ +namespace ServiceLib.Handler.Fmt; + +public class NaiveFmt : 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.Naive, + Remarks = parsedUrl.GetComponents(UriComponents.Fragment, UriFormat.Unescaped), + Address = parsedUrl.IdnHost, + Port = parsedUrl.Port, + }; + var protocolExtra = item.GetProtocolExtra(); + if (parsedUrl.Scheme.Contains("quic")) + { + protocolExtra = protocolExtra with + { + NaiveQuic = true, + }; + } + var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo); + if (rawUserInfo.Contains(':')) + { + var split = rawUserInfo.Split(':', 2); + item.Username = split[0]; + item.Password = split[1]; + } + else + { + item.Password = rawUserInfo; + } + + var query = Utils.ParseQueryString(parsedUrl.Query); + ResolveUriQuery(query, ref item); + var insecureConcurrency = int.TryParse(GetQueryValue(query, "insecure-concurrency"), out var ic) ? ic : 0; + if (insecureConcurrency > 0) + { + protocolExtra = protocolExtra with + { + InsecureConcurrency = insecureConcurrency, + }; + } + + item.SetProtocolExtra(protocolExtra); + 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 userInfo = item.Username.IsNotEmpty() ? $"{Utils.UrlEncode(item.Username)}:{Utils.UrlEncode(item.Password)}" : Utils.UrlEncode(item.Password); + var dicQuery = new Dictionary(); + ToUriQuery(item, Global.None, ref dicQuery); + var protocolExtra = item.GetProtocolExtra(); + if (protocolExtra.InsecureConcurrency > 0) + { + dicQuery.Add("insecure-concurrency", protocolExtra?.InsecureConcurrency.ToString()); + } + + var query = dicQuery.Count > 0 + ? ("?" + string.Join("&", dicQuery.Select(x => x.Key + "=" + x.Value).ToArray())) + : string.Empty; + var url = $"{userInfo}@{GetIpv6(item.Address)}:{item.Port}"; + + if (protocolExtra.NaiveQuic == true) + { + return $"{Global.NaiveQuicProtocolShare}{url}{query}{remark}"; + } + else + { + return $"{Global.NaiveHttpsProtocolShare}{url}{query}{remark}"; + } + } +} diff --git a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs index 56f78ea9..d1f802b0 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs @@ -30,7 +30,10 @@ public class TuicFmt : BaseFmt var query = Utils.ParseQueryString(url.Query); ResolveUriQuery(query, ref item); - item.HeaderType = GetQueryValue(query, "congestion_control"); + item.SetProtocolExtra(item.GetProtocolExtra() with + { + CongestionControl = GetQueryValue(query, "congestion_control") + }); return item; } @@ -51,7 +54,10 @@ public class TuicFmt : BaseFmt var dicQuery = new Dictionary(); ToUriQueryLite(item, ref dicQuery); - dicQuery.Add("congestion_control", item.HeaderType); + if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty()) + { + dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl); + } return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Username ?? ""}:{item.Password}", dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 43c5611d..35847d71 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -305,12 +305,11 @@ public sealed class AppManager return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.CoreType == eCoreType); } +#pragma warning disable CS0618 public async Task MigrateProfileExtra() { await MigrateProfileExtraGroup(); -#pragma warning disable CS0618 - const int pageSize = 100; var offset = 0; @@ -334,7 +333,6 @@ public sealed class AppManager } //await ProfileGroupItemManager.Instance.ClearAll(); -#pragma warning restore CS0618 } private async Task MigrateProfileExtraSub(List batch) @@ -380,6 +378,7 @@ public sealed class AppManager break; case EConfigType.TUIC: + extra = extra with { CongestionControl = item.HeaderType.NullIfEmpty(), }; item.Username = item.Id; item.Id = item.Security; item.Password = item.Security; @@ -436,7 +435,6 @@ public sealed class AppManager private async Task MigrateProfileExtraGroup() { -#pragma warning disable CS0618 var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); var groupItems = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!)); @@ -501,8 +499,8 @@ public sealed class AppManager return true; //await ProfileGroupItemManager.Instance.ClearAll(); -#pragma warning restore CS0618 } +#pragma warning restore CS0618 #endregion SqliteHelper diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index a55be232..14a9ae50 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -132,7 +132,7 @@ public class CoreManager return null; } - var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); + var coreType = context.RunCoreType; var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); return await RunProcess(coreInfo, fileName, true, false); } diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 7dc8c266..8583906b 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -87,7 +87,6 @@ public class MsgUIItem public class UIItem { public bool EnableAutoAdjustMainLvColWidth { get; set; } - public bool EnableUpdateSubOnlyRemarksExist { get; set; } public int MainGirdHeight1 { get; set; } public int MainGirdHeight2 { get; set; } public EGirdOrientation MainGirdOrientation { get; set; } = EGirdOrientation.Vertical; @@ -145,6 +144,7 @@ public class TunModeItem public string Stack { get; set; } public int Mtu { get; set; } public bool EnableIPv6Address { get; set; } + public string IcmpRouting { get; set; } } [Serializable] @@ -208,6 +208,7 @@ public class ClashUIItem public int ProxiesAutoDelayTestInterval { get; set; } = 10; public bool ConnectionsAutoRefresh { get; set; } public int ConnectionsRefreshInterval { get; set; } = 2; + public List ConnectionsColumnItem { get; set; } } [Serializable] diff --git a/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs b/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs index a768ba80..03403f5b 100644 --- a/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs +++ b/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs @@ -2,6 +2,9 @@ namespace ServiceLib.Models; public record ProtocolExtraItem { + public bool? Uot { get; init; } + public string? CongestionControl { get; init; } + // vmess public string? AlterId { get; init; } public string? VmessSecurity { get; init; } @@ -29,6 +32,10 @@ public record ProtocolExtraItem public string? Ports { get; init; } public string? HopInterval { get; init; } + // naiveproxy + public int? InsecureConcurrency { get; init; } + public bool? NaiveQuic { get; init; } + // group profile public string? GroupType { get; init; } public string? ChildItems { get; init; } diff --git a/v2rayN/ServiceLib/Models/ServerTestItem.cs b/v2rayN/ServiceLib/Models/ServerTestItem.cs index f1622a7c..bce76364 100644 --- a/v2rayN/ServiceLib/Models/ServerTestItem.cs +++ b/v2rayN/ServiceLib/Models/ServerTestItem.cs @@ -10,6 +10,6 @@ public class ServerTestItem public bool AllowTest { get; set; } public bool NeedAutoFillRemarks { get; set; } public int QueueNum { get; set; } - public required ProfileItem Profile { get; set; } + public ProfileItem Profile { get; set; } public ECoreType CoreType { get; set; } } diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/SingboxConfig.cs index 02deaabf..f2331cc4 100644 --- a/v2rayN/ServiceLib/Models/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs @@ -28,6 +28,7 @@ public class Dns4Sbox public bool? disable_cache { get; set; } public bool? disable_expire { get; set; } public bool? independent_cache { get; set; } + public int? cache_capacity { get; set; } public bool? reverse_mapping { get; set; } public string? client_subnet { get; set; } } @@ -133,10 +134,14 @@ public class Outbound4Sbox : BaseServer4Sbox public int? recv_window_conn { get; set; } public int? recv_window { get; set; } public bool? disable_mtu_discovery { get; set; } + public int? insecure_concurrency { get; set; } + public bool? udp_over_tcp { get; set; } public string? method { get; set; } public string? username { get; set; } public string? password { get; set; } public string? congestion_control { get; set; } + public bool? quic { get; set; } + public string? quic_congestion_control { get; set; } public string? version { get; set; } public string? network { get; set; } public string? packet_encoding { get; set; } diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index 2393d5fb..a6e6e8c7 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -179,6 +179,8 @@ public class ServersItem4Ray public string flow { get; set; } + public bool? uot { get; set; } + public List users { get; set; } } @@ -360,6 +362,7 @@ public class TlsSettings4Ray public bool? disableSystemRoot { get; set; } public string? echConfigList { get; set; } public string? echForceQuery { get; set; } + public Sockopt4Ray? echSockopt { get; set; } } public class CertificateSettings4Ray diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index b00bec2d..bd26cd9e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -691,7 +691,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add Child 的本地化字符串。 + /// 查找类似 Add Child 的本地化字符串。 /// public static string menuAddChildServer { get { @@ -718,7 +718,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [Hysteria2] 的本地化字符串。 + /// 查找类似 Add [Hysteria2] 的本地化字符串。 /// public static string menuAddHysteria2Server { get { @@ -727,7 +727,16 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add Policy Group 的本地化字符串。 + /// 查找类似 Add [NaïveProxy] 的本地化字符串。 + /// + public static string menuAddNaiveServer { + get { + return ResourceManager.GetString("menuAddNaiveServer", resourceCulture); + } + } + + /// + /// 查找类似 Add Policy Group 的本地化字符串。 /// public static string menuAddPolicyGroupServer { get { @@ -772,7 +781,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [Shadowsocks] 的本地化字符串。 + /// 查找类似 Add [Shadowsocks] 的本地化字符串。 /// public static string menuAddShadowsocksServer { get { @@ -781,7 +790,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [SOCKS] 的本地化字符串。 + /// 查找类似 Add [SOCKS] 的本地化字符串。 /// public static string menuAddSocksServer { get { @@ -790,7 +799,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [Trojan] 的本地化字符串。 + /// 查找类似 Add [Trojan] 的本地化字符串。 /// public static string menuAddTrojanServer { get { @@ -799,7 +808,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [TUIC] 的本地化字符串。 + /// 查找类似 Add [TUIC] 的本地化字符串。 /// public static string menuAddTuicServer { get { @@ -808,7 +817,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [VLESS] 的本地化字符串。 + /// 查找类似 Add [VLESS] 的本地化字符串。 /// public static string menuAddVlessServer { get { @@ -817,7 +826,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [VMess] 的本地化字符串。 + /// 查找类似 Add [VMess] 的本地化字符串。 /// public static string menuAddVmessServer { get { @@ -826,7 +835,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Add [WireGuard] 的本地化字符串。 + /// 查找类似 Add [WireGuard] 的本地化字符串。 /// public static string menuAddWireguardServer { get { @@ -924,6 +933,42 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Copy 的本地化字符串。 + /// + public static string menuEditCopy { + get { + return ResourceManager.GetString("menuEditCopy", resourceCulture); + } + } + + /// + /// 查找类似 Format 的本地化字符串。 + /// + public static string menuEditFormat { + get { + return ResourceManager.GetString("menuEditFormat", resourceCulture); + } + } + + /// + /// 查找类似 Paste 的本地化字符串。 + /// + public static string menuEditPaste { + get { + return ResourceManager.GetString("menuEditPaste", resourceCulture); + } + } + + /// + /// 查找类似 Select all 的本地化字符串。 + /// + public static string menuEditSelectAll { + get { + return ResourceManager.GetString("menuEditSelectAll", resourceCulture); + } + } + /// /// 查找类似 Edit 的本地化字符串。 /// @@ -3033,6 +3078,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 ICMP routing policy 的本地化字符串。 + /// + public static string TbIcmpRoutingPolicy { + get { + return ResourceManager.GetString("TbIcmpRoutingPolicy", resourceCulture); + } + } + /// /// 查找类似 UUID(id) 的本地化字符串。 /// @@ -3069,6 +3123,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Insecure Concurrency 的本地化字符串。 + /// + public static string TbInsecureConcurrency { + get { + return ResourceManager.GetString("TbInsecureConcurrency", resourceCulture); + } + } + /// /// 查找类似 Most Stable 的本地化字符串。 /// @@ -3376,7 +3439,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Process (Tun mode) 的本地化字符串。 + /// 查找类似 Process (Linux/Windows) 的本地化字符串。 /// public static string TbRoutingRuleProcess { get { @@ -3816,15 +3879,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Updating subscription, only determining if remarks exist 的本地化字符串。 - /// - public static string TbSettingsEnableUpdateSubOnlyRemarksExist { - get { - return ResourceManager.GetString("TbSettingsEnableUpdateSubOnlyRemarksExist", resourceCulture); - } - } - /// /// 查找类似 Exception 的本地化字符串。 /// @@ -4464,6 +4518,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 UDP over TCP 的本地化字符串。 + /// + public static string TbUot { + get { + return ResourceManager.GetString("TbUot", resourceCulture); + } + } + + /// + /// 查找类似 Username 的本地化字符串。 + /// + public static string TbUsername { + get { + return ResourceManager.GetString("TbUsername", resourceCulture); + } + } + /// /// 查找类似 Validate Regional Domain IPs 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 16836861..f95646a6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1027,7 +1027,7 @@ پروتکل sing-box Mux - Process (Tun mode) + Process (Linux/Windows) IP or IP CIDR @@ -1098,9 +1098,6 @@ آدرس اینترنتی تست پینگ سرعت - - اشتراک در حال به‌روزرسانی، فقط مشخص کنید که ملاحظاتی آیا وجود دارد! - پایان تست... @@ -1671,4 +1668,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + کپی + + + انتخاب همه + + + Paste + + + Format + + + UDP over TCP + + + Add NaïveProxy + + + Insecure Concurrency + + + Username + + + ICMP routing policy + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 253831ef..fcdab47a 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1024,7 +1024,7 @@ Protocole de multiplexage Mux (sing-box) - Process (Tun mode) + Process (Linux/Windows) IP ou IP CIDR @@ -1095,9 +1095,6 @@ Adresse de test de connexion réelle - - Ne vérifier l’existence de l’alias qu’à la maj. des abonnements - Arrêt du test en cours... @@ -1668,4 +1665,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region - + + Copier + + + Tout sélect + + + Paste + + + Format + + + UDP over TCP + + + Ajouter [NaïveProxy] + + + Insecure Concurrency + + + Username + + + ICMP routing policy + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index a29d8987..a22a476a 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1027,7 +1027,7 @@ sing-box Mux protokoll - Process (Tun mode) + Process (Linux/Windows) IP vagy IP CIDR @@ -1098,9 +1098,6 @@ Sebesség Ping Teszt URL - - Előfizetés frissítése, csak a megjegyzések létezésének ellenőrzése - Teszt megszakítása... @@ -1671,4 +1668,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + Másolás + + + Összes kijelölése + + + Paste + + + Format + + + UDP over TCP + + + [NaïveProxy] konfiguráció hozzáadása + + + Insecure Concurrency + + + Username + + + ICMP routing policy + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 17416125..26d54e51 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -514,19 +514,19 @@ Add a custom configuration - Add [Shadowsocks] + Add [Shadowsocks] - Add [SOCKS] + Add [SOCKS] - Add [Trojan] + Add [Trojan] - Add [VLESS] + Add [VLESS] - Add [VMess] + Add [VMess] Select all @@ -1027,7 +1027,7 @@ sing-box Mux Protocol - Process (Tun mode) + Process (Linux/Windows) IP or IP CIDR @@ -1036,7 +1036,7 @@ Domain - Add [Hysteria2] + Add [Hysteria2] Hysteria Max bandwidth (Up/Down) @@ -1045,7 +1045,7 @@ Use System Hosts - Add [TUIC] + Add [TUIC] Congestion control @@ -1075,7 +1075,7 @@ Enable IPv6 Address - Add [WireGuard] + Add [WireGuard] Private Key @@ -1098,9 +1098,6 @@ Speed Ping Test URL - - Updating subscription, only determining if remarks exist - Test terminating... @@ -1498,13 +1495,13 @@ Policy Group Type - Add Policy Group + Add Policy Group Add Proxy Chain - Add Child + Add Child Remove Child @@ -1671,4 +1668,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group by Region + + Copy + + + Select all + + + Paste + + + Format + + + UDP over TCP + + + Add [NaïveProxy] + + + Insecure Concurrency + + + Username + + + ICMP routing policy + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index d8a174d5..aa0594bd 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1027,7 +1027,7 @@ Протокол Mux для sing-box - Process (Tun mode) + Процесс (Linux/Windows) IP-адрес или сеть CIDR @@ -1098,9 +1098,6 @@ URL для быстрой проверки реальной задержки - - Обновляя подписку, проверять лишь наличие примечаний - Отмена тестирования... @@ -1396,10 +1393,10 @@ Внутренний DNS - Direct Target Resolution Strategy + Стратегия разрешения прямых соединений - Proxy Target Resolution Strategy + Стратегия разрешения прокси-соединений Добавить стандартные записи hosts (DNS) @@ -1432,7 +1429,7 @@ Включён пользовательский DNS — настройки на этой странице не применяются - Block ECH and HTTP/3 availability checks when enabled + При включении блокирует проверки доступности ECH и HTTP/3 Пожалуйста, заполните корректный шаблон конфигурации @@ -1465,127 +1462,127 @@ Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную. - Start parsing and processing subscription content + Начинается разбор и обработка содержимого подписки - Select Profile + Выбрать профиль - Applies globally by default, with built-in FakeIP filtering (sing-box only). + По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box). - Please Add At Least One Configuration + Добавьте хотя бы одну конфигурацию - Policy Group + Группа политик - Proxy Chain + Цепочка прокси - Lowest Latency + Наименьшая задержка - Random + Случайный - Round Robin + Циклический (Round Robin) - Most Stable + Наиболее стабильный - Policy Group Type + Тип группы политик - Add Policy Group Configuration + Добавить группу политик - Add Proxy Chain Configuration + Добавить цепочку прокси - Add Child Configuration + Добавить дочернюю конфигурацию - Remove Child Configuration + Удалить дочернюю конфигурацию - Configuration item 1, Auto add from subscription group + Конфигурация 1: автодобавление из группы подписки - Fallback + Резервный (Fallback) - Core '{0}' does not support network type '{1}' + Ядро «{0}» не поддерживает тип сети «{1}» - Core '{0}' does not support protocol '{1}' when using transport '{2}' + Ядро «{0}» не поддерживает протокол «{1}» при транспорте «{2}» - Core '{0}' does not support protocol '{1}' + Ядро «{0}» не поддерживает протокол «{1}» - The {0} property is invalid, please check + Свойство {0} недопустимо, проверьте его - Not support protocol '{0}' + Протокол «{0}» не поддерживается - If the system does not have a tray function, please do not enable it + Если в системе нет функции трея, не включайте эту опцию - You can set separate rules for Routing and DNS, or select "ALL" to apply to both + Можно задать отдельные правила для маршрутизации и DNS или выбрать «ALL» для применения к обоим - Rule Type + Тип правила Bootstrap DNS - Resolve DNS server domains, requires IP + Разрешает домены DNS-серверов, требуется IP-адрес - Test real delay + Тест реальной задержки - Auto add filtered configuration from subscription groups + Автодобавление отфильтрованных конфигураций из групп подписки - Certificate Pinning + Привязка сертификата - Pinned certificate (fill in either one) -When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. + Привязанный сертификат (заполните любое из полей) +При указании сертификат будет привязан, а «Разрешить небезопасные» отключится. -The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. +Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе. - Fetch Certificate + Получить сертификат - Fetch Certificate Chain + Получить цепочку сертификатов - Please set a valid domain + Укажите корректный домен - Certificate not set + Сертификат не задан - Certificate set + Сертификат задан - Custom PAC file path + Путь к пользовательскому PAC-файлу - Custom system proxy script file path + Путь к скрипту системного прокси - macOS displays this in the Dock (requires restart) + Отображать в Dock на macOS (требуется перезапуск) - Configuration Item 2, Select and add from self-built + Конфигурация 2: выбор и добавление из собственных EchConfigList @@ -1594,81 +1591,108 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchForceQuery - Full certificate (chain), PEM format + Полный сертификат (цепочка) в формате PEM - Certificate fingerprint (SHA-256) + Отпечаток сертификата (SHA-256) - Serve Stale + Отдавать устаревшие записи (Serve Stale) - Parallel Query + Параллельные запросы - By default, invoked only during routing for resolution + По умолчанию используется только при разрешении имён в процессе маршрутизации - By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS - If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль. - If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль. - Port hopping interval + Интервал смены портов (Port Hopping) - Configuration item preview + Предпросмотр конфигурации Finalmask - Routing rule {0} outbound node {1} warning: {2} + Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2} - Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел. - Group {0} has a cycle dependency on child node {1}. Skipping this node. + Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен. - Group {0} child node {1} warning: {2} + Группа {0}: предупреждение дочернего узла {1}: {2} - Group {0} child node {1} error: {2}. Skipping this node. + Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен. - Group {0} child group node {1} warning: {2} + Группа {0}: предупреждение дочернего узла группы {1}: {2} - Group {0} child group node {1} error: {2}. Skipping this node. + Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен. - Group {0} has no valid child node. + У группы {0} нет допустимых дочерних узлов. - Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел. - Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел. - Subscription previous proxy {0} not found. Skipping. + Предыдущий прокси подписки {0} не найден. Пропущено. - Subscription next proxy {0} not found. Skipping. + Следующий прокси подписки {0} не найден. Пропущено. - Generate Policy Group + Сгенерировать группу политик - All configurations + Все конфигурации - Group by Region + Группировка по регионам - \ No newline at end of file + + Скопировать + + + Выбрать все + + + Paste + + + Format + + + UDP over TCP + + + Добавить сервер [NaïveProxy] + + + Insecure Concurrency + + + Username + + + ICMP routing policy + + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 3a4e4924..484e2a12 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -517,16 +517,16 @@ 添加 [Shadowsocks] - 添加 [SOCKS] + 添加 [SOCKS] - 添加 [Trojan] + 添加 [Trojan] - 添加 [VLESS] + 添加 [VLESS] - 添加 [VMess] + 添加 [VMess] 全选 @@ -1024,7 +1024,7 @@ sing-box Mux 多路复用协议 - 进程 (Tun 模式) + 进程 (Linux/Windows) IP 或 IP CIDR @@ -1033,7 +1033,7 @@ Domain - 添加 [Hysteria2] + 添加 [Hysteria2] Hysteria 最大带宽 (Up/Dw) @@ -1042,7 +1042,7 @@ 使用系统 hosts - 添加 [TUIC] + 添加 [TUIC] 拥塞控制算法 @@ -1072,7 +1072,7 @@ 启用 IPv6 - 添加 [WireGuard] + 添加 [WireGuard] PrivateKey @@ -1095,9 +1095,6 @@ 真连接测试地址 - - 更新订阅时只判断别名已存在否 - 测试终止中... @@ -1105,7 +1102,7 @@ *grpc Authority - 添加 [HTTP] + 添加 [HTTP] 启用分片 (Fragment) @@ -1384,7 +1381,7 @@ Mldsa65Verify - 添加 [Anytls] + 添加 [Anytls] 远程 DNS @@ -1668,4 +1665,31 @@ 按地区分组 + + 复制 + + + 全选 + + + 粘贴 + + + 格式化 + + + UDP over TCP + + + 添加 [NaïveProxy] + + + 不安全并发 + + + 用户名 + + + ICMP 路由策略 + \ 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 c4125679..c404ebcc 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1024,7 +1024,7 @@ sing-box Mux 多路復用協定 - 行程 (Tun 模式) + 行程 (Linux/Windows) IP 或 IP CIDR @@ -1095,9 +1095,6 @@ 真連線測試位址 - - 更新訂閱時只判斷別名是否存在 - 測試終止中... @@ -1668,4 +1665,31 @@ 按區域分組 - \ No newline at end of file + + 複製 + + + 全選 + + + 貼上 + + + 格式化 + + + UDP over TCP + + + 新增 [NaïveProxy] 節點 + + + 不安全的並行處理 + + + 使用者名稱 + + + ICMP 路由策略 + + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 7a5074bb..f09d3ee9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -93,7 +93,23 @@ public partial class CoreConfigSingboxService foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) { - hostsDns.predefined[kvp.Key] = kvp.Value.Where(Utils.IsIpAddress).ToList(); + // only allow full match + // like example.com and full:example.com, + // but not domain:example.com, keyword:example.com or regex:example.com etc. + var testRule = new Rule4Sbox(); + if (!ParseV2Domain(kvp.Key, testRule)) + { + continue; + } + if (testRule.domain_keyword?.Count > 0 && !kvp.Key.Contains(':')) + { + testRule.domain = testRule.domain_keyword; + testRule.domain_keyword = null; + } + if (testRule.domain?.Count == 1) + { + hostsDns.predefined[testRule.domain.First()] = kvp.Value.Where(Utils.IsIpAddress).ToList(); + } } foreach (var host in hostsDns.predefined) @@ -179,44 +195,65 @@ public partial class CoreConfigSingboxService foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts)) { var predefined = kvp.Value.First(); - if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined)) + if (predefined.IsNullOrEmpty()) { continue; } - if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) + var rule = new Rule4Sbox() { - // xray syntactic sugar for predefined - // etc. #0 -> NOERROR - _coreConfig.dns.rules.Add(new() - { - query_type = [1, 28], - domain = [kvp.Key], - action = "predefined", - rcode = rcode switch - { - 0 => "NOERROR", - 1 => "FORMERR", - 2 => "SERVFAIL", - 3 => "NXDOMAIN", - 4 => "NOTIMP", - 5 => "REFUSED", - _ => "NOERROR", - }, - }); - continue; - } - // CNAME record - Rule4Sbox rule = new() - { - query_type = [1, 28], + query_type = [1, 5, 28], // A, CNAME and AAAA action = "predefined", rcode = "NOERROR", - answer = [$"*. IN CNAME {predefined}."], }; - if (ParseV2Domain(kvp.Key, rule)) + if (!ParseV2Domain(kvp.Key, rule)) { - _coreConfig.dns.rules.Add(rule); + continue; } + // see: https://xtls.github.io/en/config/dns.html#dnsobject + // The matching format (domain:, full:, etc.) is the same as the domain + // in the commonly used Routing System. The difference is that without a prefix, + // it defaults to using the full: prefix (similar to the common hosts file syntax). + if (rule.domain_keyword?.Count > 0 && !kvp.Key.Contains(':')) + { + rule.domain = rule.domain_keyword; + rule.domain_keyword = null; + } + // example.com #0 -> example.com with NOERROR + if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) + { + rule.rcode = rcode switch + { + 0 => "NOERROR", + 1 => "FORMERR", + 2 => "SERVFAIL", + 3 => "NXDOMAIN", + 4 => "NOTIMP", + 5 => "REFUSED", + _ => "NOERROR", + }; + } + else if (Utils.IsDomain(predefined)) + { + // example.com CNAME target.com -> example.com with CNAME target.com + rule.answer = new List { $"*. IN CNAME {predefined}." }; + } + else if (Utils.IsIpAddress(predefined) && (rule.domain?.Count ?? 0) == 0) + { + // not full match, but an IP address, treat it as predefined answer + if (Utils.IsIpv6(predefined)) + { + rule.answer = new List { $"*. IN AAAA {predefined}" }; + } + else + { + rule.answer = new List { $"*. IN A {predefined}" }; + } + } + else + { + continue; + } + _coreConfig.dns.rules.Add(rule); } if (simpleDnsItem.BlockBindingQuery == true) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index b30c9c48..4d4bc04e 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -112,6 +112,7 @@ public partial class CoreConfigSingboxService outbound.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod) ? protocolExtra.SsMethod : Global.None; outbound.password = _node.Password; + outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null; if (_node.Network == nameof(ETransport.tcp) && _node.HeaderType == Global.TcpHeaderHttp) { @@ -269,7 +270,7 @@ public partial class CoreConfigSingboxService { outbound.uuid = _node.Username; outbound.password = _node.Password; - outbound.congestion_control = _node.HeaderType; + outbound.congestion_control = protocolExtra.CongestionControl; break; } case EConfigType.Anytls: @@ -277,6 +278,22 @@ public partial class CoreConfigSingboxService outbound.password = _node.Password; break; } + case EConfigType.Naive: + { + outbound.username = _node.Username; + outbound.password = _node.Password; + if (protocolExtra.NaiveQuic == true) + { + outbound.quic = true; + outbound.quic_congestion_control = protocolExtra.CongestionControl.NullIfEmpty(); + } + if (protocolExtra.InsecureConcurrency > 0) + { + outbound.insecure_concurrency = protocolExtra.InsecureConcurrency; + } + outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null; + break; + } } FillOutboundTls(outbound); @@ -727,13 +744,12 @@ public partial class CoreConfigSingboxService }, null); } var idx = echConfig.IndexOf('+'); - // NOTE: query_server_name, since sing-box 1.13.0 - //var queryServerName = idx > 0 ? echConfig[..idx] : null; + var queryServerName = idx > 0 ? echConfig[..idx] : null; var echDnsServer = idx > 0 ? echConfig[(idx + 1)..] : echConfig; return (new Ech4Sbox() { enabled = true, - query_server_name = null, + query_server_name = queryServerName, }, ParseDnsAddress(echDnsServer)); } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 9bcb9adf..7e1af389 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -47,6 +47,36 @@ public partial class CoreConfigSingboxService outbound = Global.DirectTag, process_name = lstDirectExe }); + + // ICMP Routing + var icmpRouting = _config.TunModeItem.IcmpRouting ?? ""; + if (!Global.TunIcmpRoutingPolicies.Contains(icmpRouting)) + { + icmpRouting = Global.TunIcmpRoutingPolicies.First(); + } + if (icmpRouting == "direct") + { + _coreConfig.route.rules.Add(new() + { + network = ["icmp"], + outbound = Global.DirectTag, + }); + } + else if (icmpRouting != "rule") + { + var rejectMethod = icmpRouting switch + { + "unreachable" => "default", + "drop" => "drop", + _ => "reply", + }; + _coreConfig.route.rules.Add(new() + { + network = ["icmp"], + action = "reject", + method = rejectMethod, + }); + } } if (_config.Inbound.First().SniffingEnabled) @@ -84,11 +114,58 @@ public partial class CoreConfigSingboxService } if (hostsDomains.Count > 0) { - _coreConfig.route.rules.Add(new() + var hostsResolveRule = new Rule4Sbox { action = "resolve", - domain = hostsDomains, - }); + }; + var hostsCounter = 0; + foreach (var host in hostsDomains) + { + var domainRule = new Rule4Sbox(); + if (!ParseV2Domain(host, domainRule)) + { + continue; + } + if (domainRule.domain_keyword?.Count > 0 && !host.Contains(':')) + { + domainRule.domain = domainRule.domain_keyword; + domainRule.domain_keyword = null; + } + if (domainRule.domain?.Count > 0) + { + hostsResolveRule.domain ??= []; + hostsResolveRule.domain.AddRange(domainRule.domain); + hostsCounter++; + } + else if (domainRule.domain_keyword?.Count > 0) + { + hostsResolveRule.domain_keyword ??= []; + hostsResolveRule.domain_keyword.AddRange(domainRule.domain_keyword); + hostsCounter++; + } + else if (domainRule.domain_suffix?.Count > 0) + { + hostsResolveRule.domain_suffix ??= []; + hostsResolveRule.domain_suffix.AddRange(domainRule.domain_suffix); + hostsCounter++; + } + else if (domainRule.domain_regex?.Count > 0) + { + hostsResolveRule.domain_regex ??= []; + hostsResolveRule.domain_regex.AddRange(domainRule.domain_regex); + hostsCounter++; + } + else if (domainRule.geosite?.Count > 0) + { + hostsResolveRule.geosite ??= []; + hostsResolveRule.geosite.AddRange(domainRule.geosite); + hostsCounter++; + } + } + if (hostsCounter > 0) + { + _coreConfig.route.rules.Add(hostsResolveRule); + } } _coreConfig.route.rules.Add(new() @@ -355,6 +432,11 @@ public partial class CoreConfigSingboxService rule.domain_keyword ??= []; rule.domain_keyword?.Add(domain.Substring(8)); } + else if (domain.StartsWith("dotless:")) + { + rule.domain_keyword ??= []; + rule.domain_keyword?.Add(domain.Substring(8)); + } else { rule.domain_keyword ??= []; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs index ca8eeb0d..545e713e 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -316,16 +316,24 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) SsMethod = Global.None, }); - foreach (var outbound in _coreConfig.outbounds.Where(outbound => outbound.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) + foreach (var outbound in _coreConfig.outbounds + .Where(o => o.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) { - outbound.streamSettings ??= new StreamSettings4Ray(); - outbound.streamSettings.sockopt ??= new Sockopt4Ray(); - outbound.streamSettings.sockopt.dialerProxy = "tun-project-ss"; + outbound.streamSettings ??= new(); + outbound.streamSettings.sockopt ??= new(); + outbound.streamSettings.sockopt.dialerProxy = "tun-protect-ss"; + } + // ech protected + foreach (var outbound in _coreConfig.outbounds + .Where(outbound => outbound.streamSettings?.tlsSettings?.echConfigList?.IsNullOrEmpty() == false)) + { + outbound.streamSettings!.tlsSettings!.echSockopt ??= new(); + outbound.streamSettings.tlsSettings.echSockopt.dialerProxy = "tun-protect-ss"; } _coreConfig.outbounds.Add(new CoreConfigV2rayService(context with { Node = protectNode, - }).BuildProxyOutbound("tun-project-ss")); + }).BuildProxyOutbound("tun-protect-ss")); _coreConfig.routing.rules ??= []; var hasBalancer = _coreConfig.routing.balancers is { Count: > 0 }; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index c8862648..853ada7f 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -153,6 +153,7 @@ public partial class CoreConfigV2rayService serversItem.password = _node.Password; serversItem.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod) ? protocolExtra.SsMethod : "none"; + serversItem.uot = protocolExtra.Uot == true ? true : null; serversItem.ota = false; serversItem.level = 1; diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index d368108c..f9d9a5f8 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -363,44 +363,36 @@ public class UpdateService(Config config, Func updateFunc) var geoipFiles = new List(); var geoSiteFiles = new List(); - //Collect used files list + // Collect from routing rules var routingItems = await AppManager.Instance.RoutingItems(); foreach (var routing in routingItems) { var rules = JsonUtils.Deserialize>(routing.RuleSet); foreach (var item in rules ?? []) { - foreach (var ip in item.Ip ?? []) - { - var prefix = "geoip:"; - if (ip.StartsWith(prefix)) - { - geoipFiles.Add(ip.Substring(prefix.Length)); - } - } - - foreach (var domain in item.Domain ?? []) - { - var prefix = "geosite:"; - if (domain.StartsWith(prefix)) - { - geoSiteFiles.Add(domain.Substring(prefix.Length)); - } - } + AddPrefixedItems(item.Ip, "geoip:", geoipFiles); + AddPrefixedItems(item.Domain, "geosite:", geoSiteFiles); } } - //append dns items TODO - geoSiteFiles.Add("google"); - geoSiteFiles.Add("cn"); - geoSiteFiles.Add("geolocation-cn"); - geoSiteFiles.Add("category-ads-all"); + // Collect from DNS configuration + var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + if (dnsItem != null) + { + ExtractDnsRuleSets(dnsItem.NormalDNS, geoipFiles, geoSiteFiles); + ExtractDnsRuleSets(dnsItem.TunDNS, geoipFiles, geoSiteFiles); + } + // Append default items + geoSiteFiles.AddRange(["google", "cn", "geolocation-cn", "category-ads-all"]); + + // Download files var path = Utils.GetBinPath("srss"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } + foreach (var item in geoipFiles.Distinct()) { await UpdateSrsFile("geoip", item); @@ -412,6 +404,63 @@ public class UpdateService(Config config, Func updateFunc) } } + private void AddPrefixedItems(List? items, string prefix, List output) + { + if (items == null) + { + return; + } + + foreach (var item in items) + { + if (item.StartsWith(prefix)) + { + output.Add(item.Substring(prefix.Length)); + } + } + } + + private void ExtractDnsRuleSets(string? dnsJson, List geoipFiles, List geoSiteFiles) + { + if (string.IsNullOrEmpty(dnsJson)) + { + return; + } + + try + { + var dns = JsonUtils.Deserialize(dnsJson); + if (dns?.rules != null) + { + foreach (var rule in dns.rules) + { + ExtractSrsRuleSets(rule, geoipFiles, geoSiteFiles); + } + } + } + catch { } + } + + private void ExtractSrsRuleSets(Rule4Sbox? rule, List geoipFiles, List geoSiteFiles) + { + if (rule == null) + { + return; + } + + AddPrefixedItems(rule.rule_set, "geosite-", geoSiteFiles); + AddPrefixedItems(rule.rule_set, "geoip-", geoipFiles); + + // Handle nested rules recursively + if (rule.rules != null) + { + foreach (var nestedRule in rule.rules) + { + ExtractSrsRuleSets(nestedRule, geoipFiles, geoSiteFiles); + } + } + } + private async Task UpdateSrsFile(string type, string srsName) { var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 86a807db..05eb76f7 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -27,10 +27,10 @@ public class AddServerViewModel : MyReactiveObject public string Ports { get; set; } [Reactive] - public int UpMbps { get; set; } + public int? UpMbps { get; set; } [Reactive] - public int DownMbps { get; set; } + public int? DownMbps { get; set; } [Reactive] public string HopInterval { get; set; } @@ -61,6 +61,18 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public int WgMtu { get; set; } + [Reactive] + public bool Uot { get; set; } + + [Reactive] + public string CongestionControl { get; set; } + + [Reactive] + public int? InsecureConcurrency { get; set; } + + [Reactive] + public bool NaiveQuic { get; set; } + public ReactiveCommand FetchCertCmd { get; } public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } @@ -113,8 +125,8 @@ public class AddServerViewModel : MyReactiveObject AlterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0; Flow = protocolExtra?.Flow ?? string.Empty; SalamanderPass = protocolExtra?.SalamanderPass ?? string.Empty; - UpMbps = protocolExtra?.UpMbps ?? _config.HysteriaItem.UpMbps; - DownMbps = protocolExtra?.DownMbps ?? _config.HysteriaItem.DownMbps; + UpMbps = protocolExtra?.UpMbps; + DownMbps = protocolExtra?.DownMbps; HopInterval = protocolExtra?.HopInterval.IsNullOrEmpty() ?? true ? Global.Hysteria2DefaultHopInt.ToString() : protocolExtra.HopInterval; VmessSecurity = protocolExtra?.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity; VlessEncryption = protocolExtra?.VlessEncryption.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None; @@ -123,6 +135,10 @@ public class AddServerViewModel : MyReactiveObject WgInterfaceAddress = protocolExtra?.WgInterfaceAddress ?? string.Empty; WgReserved = protocolExtra?.WgReserved ?? string.Empty; WgMtu = protocolExtra?.WgMtu ?? 1280; + Uot = protocolExtra?.Uot ?? false; + CongestionControl = protocolExtra?.CongestionControl ?? string.Empty; + InsecureConcurrency = protocolExtra?.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null; + NaiveQuic = protocolExtra?.NaiveQuic ?? false; } private async Task SaveServerAsync() @@ -169,8 +185,8 @@ public class AddServerViewModel : MyReactiveObject AlterId = AlterId > 0 ? AlterId.ToString() : null, Flow = Flow.NullIfEmpty(), SalamanderPass = SalamanderPass.NullIfEmpty(), - UpMbps = UpMbps >= 0 ? UpMbps : null, - DownMbps = DownMbps >= 0 ? DownMbps : null, + UpMbps = UpMbps, + DownMbps = DownMbps, HopInterval = HopInterval.NullIfEmpty(), VmessSecurity = VmessSecurity.NullIfEmpty(), VlessEncryption = VlessEncryption.NullIfEmpty(), @@ -179,6 +195,10 @@ public class AddServerViewModel : MyReactiveObject WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(), WgReserved = WgReserved.NullIfEmpty(), WgMtu = WgMtu >= 576 ? WgMtu : null, + Uot = Uot ? true : null, + CongestionControl = CongestionControl.NullIfEmpty(), + InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null, + NaiveQuic = NaiveQuic ? true : null, }); if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index 7bcb36ea..dc31b41b 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -204,7 +204,7 @@ public class CheckUpdateViewModel : MyReactiveObject private async Task UpdateFinishedSub(bool blReload) { - RxApp.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) => + RxSchedulers.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) => { _ = UpdateFinishedResult(blReload); return Disposable.Empty; @@ -317,7 +317,7 @@ public class CheckUpdateViewModel : MyReactiveObject Remarks = msg, }; - RxApp.MainThreadScheduler.Schedule(item, (scheduler, model) => + RxSchedulers.MainThreadScheduler.Schedule(item, (scheduler, model) => { _ = UpdateViewResult(model); return Disposable.Empty; diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs index 48500f3e..07de23f2 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -56,7 +56,7 @@ public class ClashConnectionsViewModel : MyReactiveObject return; } - RxApp.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) => + RxSchedulers.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) => { _ = RefreshConnections(model); return Disposable.Empty; diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index 6628e11d..6220719b 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -90,7 +90,7 @@ public class ClashProxiesViewModel : MyReactiveObject AppEvents.ProxiesReloadRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await ProxiesReload()); #endregion AppEvents @@ -173,7 +173,7 @@ public class ClashProxiesViewModel : MyReactiveObject if (refreshUI) { - RxApp.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups()); + RxSchedulers.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups()); } } @@ -387,7 +387,7 @@ public class ClashProxiesViewModel : MyReactiveObject } var model = new SpeedTestResult() { IndexId = item.Name, Delay = result }; - RxApp.MainThreadScheduler.Schedule(model, (scheduler, model) => + RxSchedulers.MainThreadScheduler.Schedule(model, (scheduler, model) => { _ = ProxiesDelayTestResult(model); return Disposable.Empty; diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 0bd0c588..36027b79 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -18,6 +18,7 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand AddTuicServerCmd { get; } public ReactiveCommand AddWireguardServerCmd { get; } public ReactiveCommand AddAnytlsServerCmd { get; } + public ReactiveCommand AddNaiveServerCmd { get; } public ReactiveCommand AddCustomServerCmd { get; } public ReactiveCommand AddPolicyGroupServerCmd { get; } public ReactiveCommand AddProxyChainServerCmd { get; } @@ -117,6 +118,10 @@ public class MainWindowViewModel : MyReactiveObject { await AddServerAsync(EConfigType.Anytls); }); + AddNaiveServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(EConfigType.Naive); + }); AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () => { await AddServerAsync(EConfigType.Custom); @@ -228,22 +233,22 @@ public class MainWindowViewModel : MyReactiveObject AppEvents.ReloadRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await Reload()); AppEvents.AddServerViaScanRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await AddServerViaScanAsync()); AppEvents.AddServerViaClipboardRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await AddServerViaClipboardAsync(null)); AppEvents.SubscriptionsUpdateRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy)); #endregion AppEvents @@ -583,7 +588,7 @@ public class MainWindowViewModel : MyReactiveObject private void ReloadResult(bool showClashUI) { - RxApp.MainThreadScheduler.Schedule(() => + RxSchedulers.MainThreadScheduler.Schedule(() => { ShowClashUI = showClashUI; TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; @@ -592,7 +597,7 @@ public class MainWindowViewModel : MyReactiveObject private void SetReloadEnabled(bool enabled) { - RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); + RxSchedulers.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); } private async Task LoadCore(CoreConfigContext? mainContext, CoreConfigContext? preContext) diff --git a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs index 68bf71b5..fbe06cb0 100644 --- a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs @@ -31,7 +31,7 @@ public class MsgViewModel : MyReactiveObject AppEvents.SendMsgViewRequested .AsObservable() - //.ObserveOn(RxApp.MainThreadScheduler) + //.ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(content => _ = AppendQueueMsg(content)); } @@ -86,15 +86,25 @@ public class MsgViewModel : MyReactiveObject } catch (Exception ex) { - _queueMsg.Enqueue(ex.Message); + EnqueueWithLimit(ex.Message); _lastMsgFilterNotAvailable = true; } } - _queueMsg.Enqueue(msg); + EnqueueWithLimit(msg); if (!msg.EndsWith(Environment.NewLine)) { - _queueMsg.Enqueue(Environment.NewLine); + EnqueueWithLimit(Environment.NewLine); + } + } + + private void EnqueueWithLimit(string item) + { + _queueMsg.Enqueue(item); + + while (_queueMsg.Count > NumMaxMsg) + { + _queueMsg.TryDequeue(out _); } } diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 8462a2c3..5df75264 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -22,8 +22,8 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public string defUserAgent { get; set; } [Reactive] public string mux4SboxProtocol { get; set; } [Reactive] public bool enableCacheFile4Sbox { get; set; } - [Reactive] public int hyUpMbps { get; set; } - [Reactive] public int hyDownMbps { get; set; } + [Reactive] public int? hyUpMbps { get; set; } + [Reactive] public int? hyDownMbps { get; set; } [Reactive] public bool enableFragment { get; set; } #endregion Core @@ -47,7 +47,6 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public bool KeepOlderDedupl { get; set; } [Reactive] public bool DisplayRealTimeSpeed { get; set; } [Reactive] public bool EnableAutoAdjustMainLvColWidth { get; set; } - [Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; } [Reactive] public bool AutoHideStartup { get; set; } [Reactive] public bool Hide2TrayWhenClose { get; set; } [Reactive] public bool MacOSShowInDock { get; set; } @@ -96,6 +95,7 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public string TunStack { get; set; } [Reactive] public int TunMtu { get; set; } [Reactive] public bool TunEnableIPv6Address { get; set; } + [Reactive] public string TunIcmpRouting { get; set; } #endregion Tun mode @@ -180,7 +180,6 @@ public class OptionSettingViewModel : MyReactiveObject DisplayRealTimeSpeed = _config.GuiItem.DisplayRealTimeSpeed; KeepOlderDedupl = _config.GuiItem.KeepOlderDedupl; EnableAutoAdjustMainLvColWidth = _config.UiItem.EnableAutoAdjustMainLvColWidth; - EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist; AutoHideStartup = _config.UiItem.AutoHideStartup; Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose; MacOSShowInDock = _config.UiItem.MacOSShowInDock; @@ -220,6 +219,7 @@ public class OptionSettingViewModel : MyReactiveObject TunStack = _config.TunModeItem.Stack; TunMtu = _config.TunModeItem.Mtu; TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address; + TunIcmpRouting = _config.TunModeItem.IcmpRouting; #endregion Tun mode @@ -336,8 +336,8 @@ public class OptionSettingViewModel : MyReactiveObject _config.CoreBasicItem.DefUserAgent = defUserAgent; _config.Mux4SboxItem.Protocol = mux4SboxProtocol; _config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox; - _config.HysteriaItem.UpMbps = hyUpMbps; - _config.HysteriaItem.DownMbps = hyDownMbps; + _config.HysteriaItem.UpMbps = hyUpMbps ?? 0; + _config.HysteriaItem.DownMbps = hyDownMbps ?? 0; _config.CoreBasicItem.EnableFragment = enableFragment; _config.GuiItem.AutoRun = AutoRun; @@ -345,7 +345,6 @@ public class OptionSettingViewModel : MyReactiveObject _config.GuiItem.DisplayRealTimeSpeed = DisplayRealTimeSpeed; _config.GuiItem.KeepOlderDedupl = KeepOlderDedupl; _config.UiItem.EnableAutoAdjustMainLvColWidth = EnableAutoAdjustMainLvColWidth; - _config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist; _config.UiItem.AutoHideStartup = AutoHideStartup; _config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose; _config.UiItem.MacOSShowInDock = MacOSShowInDock; @@ -379,6 +378,7 @@ public class OptionSettingViewModel : MyReactiveObject _config.TunModeItem.Stack = TunStack; _config.TunModeItem.Mtu = TunMtu; _config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address; + _config.TunModeItem.IcmpRouting = TunIcmpRouting; //coreType await SaveCoreType(); diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs index eb010c58..b3810b52 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -188,14 +188,9 @@ public class ProfilesSelectViewModel : MyReactiveObject { SubItems.Add(item); } - if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null) - { - SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId); - } - else - { - SelectedSub = SubItems.First(); - } + SelectedSub = (_config.SubIndexId.IsNotEmpty() + ? SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) + : null) ?? SubItems.LastOrDefault(); } private async Task?> GetProfileItemsEx(string subid, string filter) diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index edeec49e..6e7e183c 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -228,22 +228,22 @@ public class ProfilesViewModel : MyReactiveObject AppEvents.ProfilesRefreshRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await RefreshServersBiz()); AppEvents.SubscriptionsRefreshRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await RefreshSubscriptions()); AppEvents.DispatcherStatisticsRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); AppEvents.SetDefaultServerRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async indexId => await SetDefaultServer(indexId)); #endregion AppEvents @@ -391,14 +391,9 @@ public class ProfilesViewModel : MyReactiveObject { SubItems.Add(item); } - if (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null) - { - SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId); - } - else - { - SelectedSub = SubItems.First(); - } + SelectedSub = (_config.SubIndexId.IsNotEmpty() + ? SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) + : null) ?? SubItems.LastOrDefault(); } private async Task?> GetProfileItemsEx(string subid, string filter) @@ -732,7 +727,7 @@ public class ProfilesViewModel : MyReactiveObject _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => { - RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) => + RxSchedulers.MainThreadScheduler.Schedule(result, (scheduler, result) => { _ = SetSpeedTestResult(result); return Disposable.Empty; diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index 9269851d..b8154c16 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -200,27 +200,27 @@ public class StatusBarViewModel : MyReactiveObject AppEvents.DispatcherStatisticsRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); AppEvents.RoutingsMenuRefreshRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await RefreshRoutingsMenu()); AppEvents.TestServerRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await TestServerAvailability()); AppEvents.InboundDisplayRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await InboundDisplayStatus()); AppEvents.SysProxyChangeRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async result => await SetListenerType(result)); #endregion AppEvents @@ -243,7 +243,7 @@ public class StatusBarViewModel : MyReactiveObject { AppEvents.ProfilesRefreshRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables); } } @@ -362,7 +362,7 @@ public class StatusBarViewModel : MyReactiveObject private async Task TestServerAvailabilitySub(string msg) { - RxApp.MainThreadScheduler.Schedule(msg, (scheduler, msg) => + RxSchedulers.MainThreadScheduler.Schedule(msg, (scheduler, msg) => { _ = TestServerAvailabilityResult(msg); return Disposable.Empty; diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs index 70c43130..0ad036ec 100644 --- a/v2rayN/v2rayN.Desktop/Program.cs +++ b/v2rayN/v2rayN.Desktop/Program.cs @@ -59,7 +59,7 @@ internal class Program //.WithInterFont() .WithFontByDefault() .LogToTrace() - .UseReactiveUI(); + .UseReactiveUI(_ => { }); if (OperatingSystem.IsMacOS()) { diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index 477eca5d..4dc378b4 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" + xmlns:views="clr-namespace:v2rayN.Desktop.Views" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" Title="{x:Static resx:ResUI.menuServers}" Width="900" @@ -197,6 +198,19 @@ Width="300" Margin="{StaticResource Margin4}" /> + + + + + + + + + + + + + + + + + + + + + + - + VerticalAlignment="Center" /> @@ -749,13 +841,13 @@ - + HorizontalAlignment="Stretch" + VerticalAlignment="Center" /> diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs index fdd097b2..28f8784e 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs @@ -80,7 +80,7 @@ public partial class AddServerWindow : WindowBase cmbFingerprint.SelectedValue = string.Empty; gridFinalmask.IsVisible = false; - cmbHeaderType8.ItemsSource = Global.TuicCongestionControls; + cmbCongestionControl8.ItemsSource = Global.TuicCongestionControls; break; case EConfigType.WireGuard: @@ -94,10 +94,28 @@ public partial class AddServerWindow : WindowBase case EConfigType.Anytls: gridAnytls.IsVisible = true; + sepa2.IsVisible = false; + gridTransport.IsVisible = false; lstStreamSecurity.Add(Global.StreamSecurityReality); cmbCoreType.IsEnabled = false; gridFinalmask.IsVisible = false; break; + + case EConfigType.Naive: + gridNaive.IsVisible = true; + sepa2.IsVisible = false; + gridTransport.IsVisible = false; + cmbCoreType.IsEnabled = false; + gridFinalmask.IsVisible = false; + cmbFingerprint.IsEnabled = false; + cmbFingerprint.SelectedValue = string.Empty; + cmbAlpn.IsEnabled = false; + cmbAlpn.SelectedValue = string.Empty; + cmbAllowInsecure.IsEnabled = false; + cmbAllowInsecure.SelectedValue = string.Empty; + + cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls; + break; } cmbStreamSecurity.ItemsSource = lstStreamSecurity; @@ -122,6 +140,7 @@ public partial class AddServerWindow : WindowBase case EConfigType.Shadowsocks: this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); break; @@ -156,7 +175,7 @@ public partial class AddServerWindow : WindowBase case EConfigType.TUIC: this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType8.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl8.SelectedValue).DisposeWith(disposables); break; case EConfigType.WireGuard: @@ -168,7 +187,17 @@ public partial class AddServerWindow : WindowBase break; case EConfigType.Anytls: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId10.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId11.Text).DisposeWith(disposables); + break; + + case EConfigType.Naive: + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.togNaiveQuic12.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.cmbCongestionControl12.IsEnabled).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl12.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables); break; } this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables); diff --git a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml index 21bad138..6a9b7088 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml @@ -74,23 +74,28 @@ + Header="{x:Static resx:ResUI.TbSortingHost}" + Tag="Host" /> + Header="{x:Static resx:ResUI.TbSortingChain}" + Tag="Chain" /> + Header="{x:Static resx:ResUI.TbSortingNetwork}" + Tag="Network" /> + Header="{x:Static resx:ResUI.TbSortingType}" + Tag="Type" /> + Header="{x:Static resx:ResUI.TbSortingTime}" + Tag="Elapsed" /> diff --git a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs index 1c39df72..90690c1e 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs @@ -2,9 +2,15 @@ namespace v2rayN.Desktop.Views; public partial class ClashConnectionsView : ReactiveUserControl { + private static Config _config; + private static readonly string _tag = "ClashConnectionsView"; + public ClashConnectionsView() { InitializeComponent(); + + _config = AppManager.Instance.Config; + ViewModel = new ClashConnectionsViewModel(UpdateViewHandler); btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; @@ -19,7 +25,15 @@ public partial class ClashConnectionsView : ReactiveUserControl vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); + + AppEvents.AppExitRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(_ => StorageUI()) + .DisposeWith(disposables); }); + + RestoreUI(); } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -51,4 +65,74 @@ public partial class ClashConnectionsView : ReactiveUserControl t.Index).ToList(); + if (lvColumnItem == null) + { + return; + } + + var displayIndex = 0; + foreach (var item in lvColumnItem) + { + foreach (var item2 in lstConnections.Columns) + { + if (item2.Tag == null) + { + continue; + } + if (item2.Tag.Equals(item.Name)) + { + if (item.Width < 0) + { + item2.IsVisible = false; + } + else + { + item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel); + item2.DisplayIndex = displayIndex++; + } + } + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + private void StorageUI() + { + try + { + List lvColumnItem = new(); + foreach (var item2 in lstConnections.Columns) + { + if (item2.Tag == null) + { + continue; + } + lvColumnItem.Add(new() + { + Name = (string)item2.Tag, + Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1), + Index = item2.DisplayIndex + }); + } + _config.ClashUIItem.ConnectionsColumnItem = lvColumnItem; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + #endregion UI } diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index 705e79e2..3335785b 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -3,6 +3,7 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:local="using:v2rayN.Desktop.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" @@ -399,12 +400,7 @@ BorderBrush="Gray" BorderThickness="1" Header="HTTP/SOCKS"> - + @@ -473,12 +469,7 @@ BorderBrush="Gray" BorderThickness="1" Header="HTTP/SOCKS"> - + @@ -488,12 +479,7 @@ BorderBrush="Gray" BorderThickness="1" Header="{x:Static resx:ResUI.TbSettingsTunMode}"> - + diff --git a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml index 63f29ed0..1a2b482c 100644 --- a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" + xmlns:views="clr-namespace:v2rayN.Desktop.Views" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" Title="{x:Static resx:ResUI.menuFullConfigTemplate}" Width="900" @@ -94,12 +95,7 @@ BorderBrush="Gray" BorderThickness="1" Header="xray config template json"> - + @@ -166,12 +162,7 @@ BorderBrush="Gray" BorderThickness="1" Header="sing-box config template json"> - + @@ -181,12 +172,7 @@ BorderBrush="Gray" BorderThickness="1" Header="sing-box tun config template json"> - + diff --git a/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml new file mode 100644 index 00000000..cad0021d --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs new file mode 100644 index 00000000..98620126 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/JsonEditor.axaml.cs @@ -0,0 +1,96 @@ +using System.Text.Json; +using System.Xml; +using AvaloniaEdit.Highlighting; +using AvaloniaEdit.Highlighting.Xshd; + +namespace v2rayN.Desktop.Views; + +public partial class JsonEditor : UserControl +{ + private static readonly JsonSerializerOptions SIndentedOptions = new() { WriteIndented = true }; + + private static readonly Lazy SHighlightingDark = + new(() => BuildHighlighting(dark: true), isThreadSafe: true); + + private static readonly Lazy SHighlightingLight = + new(() => BuildHighlighting(dark: false), isThreadSafe: true); + + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text), defaultValue: string.Empty); + + public string Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public JsonEditor() + { + InitializeComponent(); + var isDark = Application.Current?.ActualThemeVariant != ThemeVariant.Light; + Editor.SyntaxHighlighting = isDark ? SHighlightingDark.Value : SHighlightingLight.Value; + Editor.TextArea.TextView.Options.EnableHyperlinks = false; + + Editor.TextChanged += (_, _) => + { + if (Text != Editor.Text) + { + SetCurrentValue(TextProperty, Editor.Text); + } + }; + + this.GetObservable(TextProperty).Subscribe(text => + { + if (Editor.Text != text) + { + Editor.Text = text ?? string.Empty; + } + }); + } + + private static IHighlightingDefinition BuildHighlighting(bool dark) + { + var keyColor = dark ? "#9CDCFE" : "#0451A5"; + var strColor = dark ? "#CE9178" : "#A31515"; + var numColor = dark ? "#B5CEA8" : "#098658"; + var kwColor = dark ? "#569CD6" : "#0000FF"; + var xshd = $""" + + + + + + + + "([^"\\]|\\.)*"(?=\s*:) + "([^"\\]|\\.)*" + -?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)? + + true + false + null + + + + """; + using var reader = XmlReader.Create(new StringReader(xshd)); + return HighlightingLoader.Load(reader, HighlightingManager.Instance); + } + + private void FormatJson_Click(object? sender, RoutedEventArgs e) + { + try + { + var obj = JsonUtils.ParseJson(Editor.Text); + Editor.Text = JsonUtils.Serialize(obj, SIndentedOptions); + } + catch + { + // ignored + } + } + + private void Copy_Click(object? sender, RoutedEventArgs e) => Editor.Copy(); + private void Paste_Click(object? sender, RoutedEventArgs e) => Editor.Paste(); + private void SelectAll_Click(object? sender, RoutedEventArgs e) => Editor.SelectAll(); +} diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml index ca59c472..8d9a5f85 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml @@ -50,6 +50,7 @@ + diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index 5c5ba9cb..996e7c83 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -72,6 +72,7 @@ public partial class MainWindow : WindowBase this.BindCommand(ViewModel, vm => vm.AddTuicServerCmd, v => v.menuAddTuicServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables); @@ -128,25 +129,25 @@ public partial class MainWindow : WindowBase AppEvents.SendSnackMsgRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async content => await DelegateSnackMsg(content)) .DisposeWith(disposables); AppEvents.AppExitRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => StorageUI()) .DisposeWith(disposables); AppEvents.ShutdownRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(content => Shutdown(content)) .DisposeWith(disposables); AppEvents.ShowHideWindowRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(blShow => ShowHideWindow(blShow)) .DisposeWith(disposables); }); diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml index 90dcfeb7..aabe80fa 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml @@ -79,8 +79,8 @@ + Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" + InputGesture="Ctrl+A" /> - - - + + + cmbmux4SboxProtocol.ItemsSource = Global.SingboxMuxs; cmbMtu.ItemsSource = Global.TunMtus; cmbStack.ItemsSource = Global.TunStacks; + cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies; cmbCoreType1.ItemsSource = Global.CoreTypes; cmbCoreType2.ItemsSource = Global.CoreTypes; @@ -86,7 +87,6 @@ public partial class OptionSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.DisplayRealTimeSpeed, v => v.togDisplayRealTimeSpeed.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.KeepOlderDedupl, v => v.togKeepOlderDedupl.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableAutoAdjustMainLvColWidth, v => v.togEnableAutoAdjustMainLvColWidth.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.EnableUpdateSubOnlyRemarksExist, v => v.togEnableUpdateSubOnlyRemarksExist.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables); @@ -115,6 +115,7 @@ public partial class OptionSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType2, v => v.cmbCoreType2.SelectedValue).DisposeWith(disposables); diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index 4ca8e793..7d803f10 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -7,6 +7,7 @@ public partial class ProfilesView : ReactiveUserControl { private static Config _config; private Window? _window; + private static readonly string _tag = "ProfilesView"; public ProfilesView() { @@ -89,13 +90,13 @@ public partial class ProfilesView : ReactiveUserControl AppEvents.AppExitRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => StorageUI()) .DisposeWith(disposables); AppEvents.AdjustMainLvColWidthRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => AutofitColumnWidth()) .DisposeWith(disposables); }); @@ -381,7 +382,7 @@ public partial class ProfilesView : ReactiveUserControl } catch (Exception ex) { - Logging.SaveLog("ProfilesView", ex); + Logging.SaveLog(_tag, ex); } } @@ -399,53 +400,67 @@ public partial class ProfilesView : ReactiveUserControl private void RestoreUI() { - var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList(); - var displayIndex = 0; - foreach (var item in lvColumnItem) + try { + var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList(); + var displayIndex = 0; + foreach (var item in lvColumnItem) + { + foreach (var item2 in lstProfiles.Columns) + { + if (item2.Tag == null) + { + continue; + } + if (item2.Tag.Equals(item.Name)) + { + if (item.Width < 0) + { + item2.IsVisible = false; + } + else + { + item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel); + item2.DisplayIndex = displayIndex++; + } + if (item.Name.ToLower().StartsWith("to")) + { + item2.IsVisible = _config.GuiItem.EnableStatistics; + } + } + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + private void StorageUI() + { + try + { + List lvColumnItem = new(); foreach (var item2 in lstProfiles.Columns) { if (item2.Tag == null) { continue; } - if (item2.Tag.Equals(item.Name)) + lvColumnItem.Add(new() { - if (item.Width < 0) - { - item2.IsVisible = false; - } - else - { - item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel); - item2.DisplayIndex = displayIndex++; - } - if (item.Name.ToLower().StartsWith("to")) - { - item2.IsVisible = _config.GuiItem.EnableStatistics; - } - } + Name = (string)item2.Tag, + Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1), + Index = item2.DisplayIndex + }); } + _config.UiItem.MainColumnItem = lvColumnItem; } - } - - private void StorageUI() - { - List lvColumnItem = new(); - foreach (var item2 in lstProfiles.Columns) + catch (Exception ex) { - if (item2.Tag == null) - { - continue; - } - lvColumnItem.Add(new() - { - Name = (string)item2.Tag, - Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1), - Index = item2.DisplayIndex - }); + Logging.SaveLog(_tag, ex); } - _config.UiItem.MainColumnItem = lvColumnItem; } #endregion UI diff --git a/v2rayN/v2rayN/App.xaml.cs b/v2rayN/v2rayN/App.xaml.cs index ca56311b..3c5f1989 100644 --- a/v2rayN/v2rayN/App.xaml.cs +++ b/v2rayN/v2rayN/App.xaml.cs @@ -39,6 +39,11 @@ public partial class App : Application } AppManager.Instance.InitComponents(); + + RxAppBuilder.CreateReactiveUIBuilder() + .WithWpf() + .BuildApp(); + base.OnStartup(e); } diff --git a/v2rayN/v2rayN/GlobalUsings.cs b/v2rayN/v2rayN/GlobalUsings.cs index 1f2d1936..fb63f7eb 100644 --- a/v2rayN/v2rayN/GlobalUsings.cs +++ b/v2rayN/v2rayN/GlobalUsings.cs @@ -20,6 +20,7 @@ global using System.Windows.Threading; global using DynamicData; global using DynamicData.Binding; global using ReactiveUI; +global using ReactiveUI.Builder; global using ReactiveUI.Fody.Helpers; global using ServiceLib; global using ServiceLib.Base; diff --git a/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayN/v2rayN/Views/AddServerWindow.xaml index e78e7f9b..1eb28d9d 100644 --- a/v2rayN/v2rayN/Views/AddServerWindow.xaml +++ b/v2rayN/v2rayN/Views/AddServerWindow.xaml @@ -277,6 +277,20 @@ Margin="{StaticResource Margin4}" Style="{StaticResource DefComboBox}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); break; @@ -151,7 +170,7 @@ public partial class AddServerWindow case EConfigType.TUIC: this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl8.Text).DisposeWith(disposables); break; case EConfigType.WireGuard: @@ -163,7 +182,16 @@ public partial class AddServerWindow break; case EConfigType.Anytls: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId10.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId11.Text).DisposeWith(disposables); + break; + + case EConfigType.Naive: + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.togNaiveQuic12.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables); break; } this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.Text).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/ClashConnectionsView.xaml b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml index 0e6ba6a7..067926ff 100644 --- a/v2rayN/v2rayN/Views/ClashConnectionsView.xaml +++ b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml @@ -2,6 +2,7 @@ x:Class="v2rayN.Views.ClashConnectionsView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:base="clr-namespace:v2rayN.Base" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -78,25 +79,30 @@ - - - - - diff --git a/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs index 0a6245a9..337b38e4 100644 --- a/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs +++ b/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs @@ -1,4 +1,5 @@ using System.Windows.Controls; +using v2rayN.Base; namespace v2rayN.Views; @@ -7,9 +8,14 @@ namespace v2rayN.Views; /// public partial class ClashConnectionsView { + private static Config _config; + private static readonly string _tag = "ClashConnectionsView"; + public ClashConnectionsView() { InitializeComponent(); + _config = AppManager.Instance.Config; + ViewModel = new ClashConnectionsViewModel(UpdateViewHandler); btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; @@ -24,7 +30,15 @@ public partial class ClashConnectionsView this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); + + AppEvents.AppExitRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(_ => StorageUI()) + .DisposeWith(disposables); }); + + RestoreUI(); } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -48,7 +62,7 @@ public partial class ClashConnectionsView } catch (Exception ex) { - Logging.SaveLog("ClashConnectionsView", ex); + Logging.SaveLog(_tag, ex); } } @@ -56,4 +70,71 @@ public partial class ClashConnectionsView { ViewModel?.ClashConnectionClose(false); } + + #region UI + + private void RestoreUI() + { + try + { + var lvColumnItem = _config.ClashUIItem?.ConnectionsColumnItem?.OrderBy(t => t.Index).ToList(); + if (lvColumnItem == null) + { + return; + } + + var displayIndex = 0; + foreach (var item in lvColumnItem) + { + foreach (var col in lstConnections.Columns.Cast()) + { + if (col.ExName == item.Name) + { + if (item.Width > 0) + { + col.Width = item.Width; + } + + col.DisplayIndex = displayIndex++; + break; + } + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + private void StorageUI() + { + try + { + List lvColumnItem = new(); + foreach (var col in lstConnections.Columns.Cast()) + { + var name = col.ExName; + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + lvColumnItem.Add(new() + { + Name = name, + Width = (int)col.ActualWidth, + Index = col.DisplayIndex + }); + } + + _config.ClashUIItem.ConnectionsColumnItem = lvColumnItem; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + #endregion UI } diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs index 90999417..2710b102 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs @@ -31,13 +31,13 @@ public partial class DNSSettingWindow this.Bind(ViewModel, vm => vm.AddCommonHosts, v => v.togAddCommonHosts.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.SelectedItem).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.SelectedItem).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.SelectedItem).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Strategy4Freedom, v => v.cmbDirectDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Strategy4Proxy, v => v.cmbRemoteDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.SelectedItem).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ParallelQuery, v => v.togParallelQuery.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ServeStale, v => v.togServeStale.IsChecked).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/MainWindow.xaml b/v2rayN/v2rayN/Views/MainWindow.xaml index 1c49ca24..f6dfe42b 100644 --- a/v2rayN/v2rayN/Views/MainWindow.xaml +++ b/v2rayN/v2rayN/Views/MainWindow.xaml @@ -124,6 +124,10 @@ x:Name="menuAddAnytlsServer" Height="{StaticResource MenuItemHeight}" Header="{x:Static resx:ResUI.menuAddAnytlsServer}" /> + diff --git a/v2rayN/v2rayN/Views/MainWindow.xaml.cs b/v2rayN/v2rayN/Views/MainWindow.xaml.cs index 93e62aad..81f2f720 100644 --- a/v2rayN/v2rayN/Views/MainWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/MainWindow.xaml.cs @@ -71,6 +71,7 @@ public partial class MainWindow this.BindCommand(ViewModel, vm => vm.AddTuicServerCmd, v => v.menuAddTuicServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables); @@ -127,25 +128,25 @@ public partial class MainWindow AppEvents.SendSnackMsgRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async content => await DelegateSnackMsg(content)) .DisposeWith(disposables); AppEvents.AppExitRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => StorageUI()) .DisposeWith(disposables); AppEvents.ShutdownRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(content => Shutdown(content)) .DisposeWith(disposables); AppEvents.ShowHideWindowRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(blShow => ShowHideWindow(blShow)) .DisposeWith(disposables); }); diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml index 8f0d24d4..41d3b8d1 100644 --- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml @@ -630,20 +630,6 @@ Margin="{StaticResource Margin8}" HorizontalAlignment="Left" /> - - - + + + vm.DisplayRealTimeSpeed, v => v.togDisplayRealTimeSpeed.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.KeepOlderDedupl, v => v.togKeepOlderDedupl.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableAutoAdjustMainLvColWidth, v => v.togEnableAutoAdjustMainLvColWidth.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.EnableUpdateSubOnlyRemarksExist, v => v.togEnableUpdateSubOnlyRemarksExist.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableDragDropSort, v => v.togEnableDragDropSort.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables); @@ -120,6 +120,7 @@ public partial class OptionSettingWindow this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType2, v => v.cmbCoreType2.Text).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs index e357044c..5122b1f4 100644 --- a/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/ProfilesSelectWindow.xaml.cs @@ -97,8 +97,7 @@ public partial class ProfilesSelectWindow private void LstProfiles_ColumnHeader_Click(object sender, RoutedEventArgs e) { - var colHeader = sender as DataGridColumnHeader; - if (colHeader == null || colHeader.TabIndex < 0 || colHeader.Column == null) + if (sender is not DataGridColumnHeader colHeader || colHeader.TabIndex < 0 || colHeader.Column == null) { return; } diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs index 46def3e2..7075d358 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs @@ -10,6 +10,7 @@ namespace v2rayN.Views; public partial class ProfilesView { private static Config _config; + private static readonly string _tag = "ProfilesView"; public ProfilesView() { @@ -83,13 +84,13 @@ public partial class ProfilesView AppEvents.AppExitRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => StorageUI()) .DisposeWith(disposables); AppEvents.AdjustMainLvColWidthRequested .AsObservable() - .ObserveOn(RxApp.MainThreadScheduler) + .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(_ => AutofitColumnWidth()) .DisposeWith(disposables); }); @@ -232,8 +233,7 @@ public partial class ProfilesView private void LstProfiles_ColumnHeader_Click(object sender, RoutedEventArgs e) { - var colHeader = sender as DataGridColumnHeader; - if (colHeader == null || colHeader.TabIndex < 0 || colHeader.Column == null) + if (sender is not DataGridColumnHeader colHeader || colHeader.TabIndex < 0 || colHeader.Column == null) { return; } @@ -339,7 +339,7 @@ public partial class ProfilesView } catch (Exception ex) { - Logging.SaveLog("ProfilesView", ex); + Logging.SaveLog(_tag, ex); } } @@ -357,46 +357,59 @@ public partial class ProfilesView private void RestoreUI() { - var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList(); - var displayIndex = 0; - foreach (var item in lvColumnItem) + try { - foreach (MyDGTextColumn item2 in lstProfiles.Columns) + var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList(); + var displayIndex = 0; + foreach (var item in lvColumnItem) { - if (item2.ExName == item.Name) + foreach (var item2 in lstProfiles.Columns.Cast()) { - if (item.Width < 0) + if (item2.ExName == item.Name) { - item2.Visibility = Visibility.Hidden; - } - else - { - item2.Width = item.Width; - item2.DisplayIndex = displayIndex++; - } - if (item.Name.ToLower().StartsWith("to")) - { - item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden; + if (item.Width < 0) + { + item2.Visibility = Visibility.Hidden; + } + else + { + item2.Width = item.Width; + item2.DisplayIndex = displayIndex++; + } + if (item.Name.ToLower().StartsWith("to")) + { + item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden; + } } } } } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } } private void StorageUI() { - List lvColumnItem = new(); - foreach (var t in lstProfiles.Columns) + try { - var item2 = (MyDGTextColumn)t; - lvColumnItem.Add(new() + List lvColumnItem = new(); + foreach (var item2 in lstProfiles.Columns.Cast()) { - Name = item2.ExName, - Width = (int)(item2.Visibility == Visibility.Visible ? item2.ActualWidth : -1), - Index = item2.DisplayIndex - }); + lvColumnItem.Add(new() + { + Name = item2.ExName, + Width = (int)(item2.Visibility == Visibility.Visible ? item2.ActualWidth : -1), + Index = item2.DisplayIndex + }); + } + _config.UiItem.MainColumnItem = lvColumnItem; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); } - _config.UiItem.MainColumnItem = lvColumnItem; } #endregion UI @@ -405,7 +418,7 @@ public partial class ProfilesView private Point startPoint = new(); private int startIndex = -1; - private string formatData = "ProfileItemModel"; + private readonly string formatData = "ProfileItemModel"; /// /// Helper to search up the VisualTree diff --git a/v2rayN/v2rayN/v2rayN.csproj b/v2rayN/v2rayN/v2rayN.csproj index 9654625d..45cbc376 100644 --- a/v2rayN/v2rayN/v2rayN.csproj +++ b/v2rayN/v2rayN/v2rayN.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows10.0.17763 + net8.0-windows10.0.19041.0 true true Resources\v2rayN.ico