diff --git a/.editorconfig b/.editorconfig index 3aa0aa54a50..0e8f9fbc5e2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -100,7 +100,6 @@ csharp_style_prefer_tuple_swap = true:warning csharp_style_prefer_utf8_string_literals = true:warning csharp_style_throw_expression = true:warning csharp_style_unused_value_assignment_preference = discard_variable:warning -csharp_style_unused_value_expression_statement_preference = discard_variable:warning csharp_using_directive_placement = outside_namespace:warning csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:warning csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index fe3af4a9743..baeae29a35d 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -49,7 +49,7 @@ jobs: libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev - name: Checkout repo (for scripts) - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: submodules: 'recursive' fetch-depth: '0' @@ -151,7 +151,7 @@ jobs: dnf repolist | grep -i epel || true - name: Checkout repo (for scripts) - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: submodules: 'recursive' fetch-depth: '0' @@ -251,3 +251,252 @@ jobs: --data-binary @"$f" \ "${upload_url}?name=${f##*/}" done + + deb-riscv64: + name: build and release deb riscv64 + if: | + (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + runs-on: ubuntu-24.04-riscv + container: debian:13 + env: + RELEASE_TAG: ${{ case(inputs.release_tag != '', 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 \ + gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 + + - name: Checkout repo (for scripts) + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + rm -rf ./* + git init . + git config --global --add safe.directory "$PWD" + git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + git submodule update --init --recursive + + - name: Ensure script permissions + run: chmod 755 package-debian-riscv.sh + + - name: Package DEB (Debian-family) + run: ./package-debian-riscv.sh "${RELEASE_TAG}" + + - name: Collect DEBs into workspace + run: | + mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64" + rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true + find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" \ + -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true + echo "==== Dist tree ====" + ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true + + - name: Upload DEBs to release + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + shopt -s globstar nullglob + + files=(dist/deb-riscv64/**/*.deb) + (( ${#files[@]} )) || { echo "No DEBs found."; exit 1; } + + api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" + upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')" + [[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; } + + for f in "${files[@]}"; do + echo "Uploading ${f##*/}" + curl -fsSL -X POST \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H "Content-Type: application/vnd.debian.binary-package" \ + --data-binary @"$f" \ + "${upload_url}?name=${f##*/}" + done + + deb-loong64: + name: build and release deb loong64 + if: | + (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + runs-on: ubuntu-24.04 + + env: + RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} + QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2 + EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd + EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd + QCOW2_IMAGE: debian13-loong64.qcow2 + EFI_CODE: edk2-loongarch64-code.fd + EFI_VARS: edk2-loongarch64-vars.fd + QEMU_VERSION: 10.2.3 + + steps: + - name: Prepare host tools + shell: bash + run: | + set -euo pipefail + + sudo apt-get update + sudo apt-get install -y rsync qemu-utils expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 + + - name: Checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 0 + + - name: Download QEMU prebuild + run: | + set -euo pipefail + + wget -O qemu-linux-x64.tar.gz \ + "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64.tar.gz" + + tar -xzf qemu-linux-x64.tar.gz + + mkdir -p "$HOME/qemu-install" + + rsync -a qemu-linux-x64/ "$HOME/qemu-install/" + + "$HOME/qemu-install/bin/qemu-system-loongarch64" --version + + - name: Download loong64 qcow2 and EFI firmware + shell: bash + run: | + set -euo pipefail + + wget -O "$QCOW2_IMAGE" "$QCOW2_URL" + wget -O "$EFI_CODE" "$EFI_CODE_URL" + wget -O "$EFI_VARS" "$EFI_VARS_URL" + + qemu-img info "$QCOW2_IMAGE" + + - name: Build loong64 DEB in VM through serial console + shell: bash + timeout-minutes: 180 + env: + RELEASE_TAG: ${{ env.RELEASE_TAG }} + run: | + set -euo pipefail + + mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64" + + expect <<'EOF' + log_user 1 + set timeout -1 + + set release_tag $env(RELEASE_TAG) + set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64" + set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu" + + set workspace $env(GITHUB_WORKSPACE) + set repo $env(GITHUB_REPOSITORY) + set sha $env(GITHUB_SHA) + + set qcow2 $env(QCOW2_IMAGE) + set efi_code $env(EFI_CODE) + set efi_vars $env(EFI_VARS) + + proc wait_prompt {} { + expect { + -re "__CI_PROMPT__# " {} + timeout { exit 1 } + eof { exit 1 } + } + } + + proc run_cmd {cmd} { + send -- "$cmd\r" + wait_prompt + } + + spawn $qemu_bin \ + -L $qemu_rom_dir \ + -machine virt \ + -accel tcg,thread=multi,tb-size=2048 \ + -cpu la464 \ + -m 9216 \ + -smp 4 \ + -drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \ + -drive if=pflash,format=raw,unit=1,file=$efi_vars \ + -device virtio-blk-pci,drive=hd0,bootindex=1 \ + -drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \ + -netdev user,id=net0 \ + -device virtio-net-pci,netdev=net0 \ + -virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \ + -display none \ + -serial stdio \ + -monitor none + + expect -re "login:|debian login:" + send -- "root\r" + + expect -re "Password:|密码:|密码:" + send -- "password\r" + + expect { + -re "# " {} + timeout { exit 1 } + eof { exit 1 } + } + + send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r" + wait_prompt + + run_cmd "mkdir -p /workspace" + run_cmd "mount -t 9p -o trans=virtio,version=9p2000.L workspace /workspace || mount -t 9p -o trans=virtio workspace /workspace" + run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true" + run_cmd "dhclient \$IFACE || true" + run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf" + run_cmd "apt-get update || apt-get update || apt-get update" + run_cmd "DEBIAN_FRONTEND=noninteractive 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 gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1" + run_cmd "rm -rf /root/v2rayN-src" + run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src" + run_cmd "cd /root/v2rayN-src && git fetch --depth=1 origin $sha && git checkout FETCH_HEAD && git submodule update --init --recursive" + run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh" + + send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r" + + expect { + -re "__BUILD_DONE__0" { + send -- "poweroff\r" + } + default { + exit 1 + } + } + EOF + + - name: Collect DEBs + shell: bash + run: | + set -euo pipefail + + mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64" + + find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" \ + -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true + + echo "==== Dist tree ====" + ls -R "$GITHUB_WORKSPACE/dist/deb-loong64" + + - name: Upload DEBs to release + uses: svenstaro/upload-release-action@v2 + with: + file: dist/deb-loong64/**/*.deb + tag: ${{ env.RELEASE_TAG }} + file_glob: true + prerelease: true diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index 3db6f621da3..340fc32f701 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -45,7 +45,7 @@ jobs: }} steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 - name: Restore build artifacts uses: actions/download-artifact@v8 @@ -63,7 +63,7 @@ jobs: run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }} - name: Sleep for race condition between matrix jobs - run: sleep $(awk 'BEGIN { srand(); printf "%.3f", rand()*2 }') + run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')" - name: Upload dmg to release uses: svenstaro/upload-release-action@v2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 838a99ec0a1..8c73a6c09e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,15 +46,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: submodules: 'recursive' fetch-depth: '0' - name: Setup .NET - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@v5.3.0 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.1xx' - name: Build v2rayN working-directory: ./v2rayN @@ -68,4 +68,4 @@ jobs: uses: actions/upload-artifact@v7.0.1 with: name: ${{ matrix.arch }} - path: ${{ matrix.arch }} + path: ${{ matrix.arch }} \ No newline at end of file diff --git a/.github/workflows/package-zip.yml b/.github/workflows/package-zip.yml index d724841a459..1f9b3613996 100644 --- a/.github/workflows/package-zip.yml +++ b/.github/workflows/package-zip.yml @@ -37,7 +37,7 @@ jobs: }} steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 - name: Restore build artifacts uses: actions/download-artifact@v8 @@ -56,8 +56,8 @@ jobs: run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip" - name: Sleep for race condition between matrix jobs - run: sleep $(awk 'BEGIN { srand(); printf "%.3f", rand()*2 }') - + run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')" + - name: Upload zip archive to release uses: svenstaro/upload-release-action@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9364283fd6b..633bb8e2e97 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,13 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: submodules: 'recursive' fetch-depth: '0' - name: Setup .NET - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@v5.3.0 with: dotnet-version: '8.0.x' diff --git a/package-debian-loong.sh b/package-debian-loong.sh new file mode 100644 index 00000000000..e00b0cbf3a3 --- /dev/null +++ b/package-debian-loong.sh @@ -0,0 +1,742 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION_ARG="" +WITH_CORE="both" +FORCE_NETCORE=0 +BUILD_FROM="" +XRAY_VER="${XRAY_VER:-}" +SING_VER="${SING_VER:-}" + +MIN_KERNEL="5.10" +PKGROOT="v2rayN-publish" +PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" +OUTPUT_DIR="${HOME}/debbuild" +DOTNET_TFM="net10.0" +DOTNET_LOONGARCH_VERSION="10.0.108" +DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64" +DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" +DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" +DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" + +OS_ID="" +OS_NAME="" +OS_VERSION_ID="" +HOST_ARCH="" +SCRIPT_DIR="" +PROJECT="" +VERSION="" + +declare -a BUILT_DEBS=() + +die() { + echo "$*" >&2 + exit 1 +} + +parse_args() { + local first_arg="${1:-}" + + if [[ -n "$first_arg" && "$first_arg" != --* ]]; then + VERSION_ARG="$first_arg" + shift || true + fi + + 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 ;; + --buildfrom) BUILD_FROM="${2:-}"; shift 2 ;; + *) + [[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1" + shift + ;; + esac + done + + if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then + die "You cannot specify both an explicit version and --buildfrom at the same time. + Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." + fi +} + +detect_environment() { + local current_kernel="" + local lowest="" + + . /etc/os-release + + OS_ID="${ID:-}" + OS_NAME="${NAME:-$OS_ID}" + OS_VERSION_ID="${VERSION_ID:-}" + HOST_ARCH="$(uname -m)" + + case "$OS_ID" in + debian) + echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}" + ;; + *) + die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}). +This script only supports: Debian." + ;; + esac + + case "$HOST_ARCH" in + loongarch64) ;; + *) die "Only supports loongarch64" ;; + esac + + current_kernel="$(uname -r)" + lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)" + + [[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL" + echo "[OK] Kernel $current_kernel verified." +} + +install_dependencies() { + local install_ok=0 + local tmp_dotnet="" + + mkdir -p "$OUTPUT_DIR" + + 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 gcc make pkg-config \ + libicu-dev libssl-dev libfontconfig1 libfreetype6 zlib1g + + mkdir -p "$HOME/.dotnet" + tmp_dotnet="$(mktemp -d)" + curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_LOONGARCH_FILE" + tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_LOONGARCH_FILE" + rm -rf "$tmp_dotnet" + + export PATH="$HOME/.dotnet:$PATH" + export DOTNET_ROOT="$HOME/.dotnet" + + mkdir -p "$HOME/.nuget/NuGet" + + cat > "$HOME/.nuget/NuGet/NuGet.Config" < + + + + + + + +EOF + + dotnet --info >/dev/null 2>&1 && install_ok=1 + fi + + if [[ "$install_ok" -ne 1 ]]; then + echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:" + echo "dotnet-loongarch SDK, curl, unzip, tar, rsync, git, gcc, make, dpkg-deb, fakeroot, libicu-dev, libssl-dev" + exit 1 + fi +} + +prepare_workspace() { + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + cd "$SCRIPT_DIR" + + if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true + fi + + PROJECT="$PROJECT_HINT" + [[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" + [[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found" +} + +choose_channel() { + local ch="latest" + local sel="" + + if [[ -n "${BUILD_FROM:-}" ]]; then + case "$BUILD_FROM" in + 1) echo "latest"; return 0 ;; + 2) echo "prerelease"; return 0 ;; + 3) echo "keep"; return 0 ;; + *) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;; + esac + fi + + 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 + git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want" + + if [[ -n "$ref" ]]; then + echo "[OK] Found ref '${ref}', checking out..." + git checkout -f "$ref" + sync_submodules + return 0 + fi + fi + + return 1 +} + +apply_channel_or_keep() { + local ch="$1" + local 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..." + + case "$ch" in + latest) tag="$(get_latest_tag_latest || true)" ;; + prerelease) tag="$(get_latest_tag_prerelease || true)" ;; + *) die "Failed to resolve latest tag for channel '${ch}'." ;; + esac + + [[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'." + + echo "[*] Latest tag for '${ch}': ${tag}" + git_try_checkout "$tag" || die "Failed to checkout '${tag}'." + VERSION="${tag#v}" +} + +resolve_version() { + if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + local clean_ver="${VERSION_ARG#v}" + + if git_try_checkout "$clean_ver"; then + VERSION="$clean_ver" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + apply_channel_or_keep "$(choose_channel)" + fi + else + apply_channel_or_keep "$(choose_channel)" + 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}" +} + +xray_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-loongarch64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-loong64.zip" ;; + *) return 1 ;; + esac +} + +singbox_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-loongarch64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-loong64.tar.gz" ;; + *) return 1 ;; + esac +} + +bundle_url_for_rid() { + local rid="$1" + + case "$rid" in + linux-loongarch64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-loong64.zip" ;; + *) return 1 ;; + esac +} + +download_xray() { + local outdir="$1" + local rid="$2" + local ver="${XRAY_VER:-}" + local url="" + local tmp="" + + 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; } + url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; } + + echo "[+] Download xray: $url" + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; } + unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; } + install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; } + rm -rf "$tmp" +} + +download_singbox() { + local outdir="$1" + local rid="$2" + local ver="${SING_VER:-}" + local url="" + local tmp="" + local bin="" + local cronet="" + + 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; } + url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; } + + echo "[+] Download sing-box: $url" + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + + 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"; return 1; } + + cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)" + [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true + + rm -rf "$tmp" +} + +unify_geo_layout() { + local outroot="$1" + local n + local names=( + geosite.dat + geoip.dat + geoip-only-cn-private.dat + Country.mmdb + geoip.metadb + ) + + mkdir -p "$outroot/bin" + + 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" + local f="" + + 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" + + 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/refs/heads/rule-set-geoip/$f" + 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/refs/heads/rule-set-geosite/$f" + done + + unify_geo_layout "$outroot" +} + +populate_assets_zip_mode() { + local outroot="$1" + local rid="$2" + local url="" + local tmp="" + local nested_dir="" + + url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; } + + echo "[+] Try v2rayN bundle archive: $url" + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; } + unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; 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 + + 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_geo_layout "$outroot" + rm -rf "$tmp" + + echo "[+] Bundle extracted to $outroot" +} + +populate_assets_netcore_mode() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" + + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)" + fi + + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)" + fi + + download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" +} + +stage_runtime_assets() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" + + if [[ "$FORCE_NETCORE" -eq 0 ]]; then + if populate_assets_zip_mode "$outroot" "$rid"; then + echo "[*] Using v2rayN bundle archive." + else + echo "[*] Bundle failed, fallback to separate core + rules." + populate_assets_netcore_mode "$outroot" "$rid" + fi + else + echo "[*] --netcore specified: use separate core + rules." + populate_assets_netcore_mode "$outroot" "$rid" + fi +} + +describe_target() { + local short="$1" + + case "$short" in + loongarch64) printf '%s\n%s\n' "linux-loongarch64" "loong64" ;; + *) echo "Unknown arch '$short' (use loongarch64)" >&2; return 1 ;; + esac +} + +publish_binary() { + local rid="$1" + + dotnet clean "$PROJECT" -c Release + rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true + dotnet restore "$PROJECT" + dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true +} + +write_launcher_file() { + local stage="$1" + + 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 +} + +write_desktop_file() { + local stage="$1" + + install -m 644 /dev/stdin "$stage/usr/share/applications/v2rayn.desktop" <<'EOF' +[Desktop Entry] +Type=Application +Name=v2rayN +Comment=v2rayN for Debian GNU Linux +Exec=v2rayn +Icon=v2rayn +Terminal=false +Categories=Network; +EOF +} + +write_maintainer_scripts() { + local debian_dir="$1" + + install -m 755 /dev/stdin "$debian_dir/postinst" <<'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 + + 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 +} + +package_binary() { + local short="$1" + local rid="$2" + local deb_arch="$3" + local pubdir="" + local workdir="" + local stage="" + local debian_dir="" + local project_dir="" + local icon_candidate="" + local shlibs_depends="" + local extra_depends="" + local final_depends="" + local multiarch="" + local sys_libdir="" + local sys_usrlibdir="" + local deb_out="" + + pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish" + [[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; } + + workdir="$(mktemp -d)" + trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN + + stage="$workdir/${PKGROOT}_${VERSION}_${deb_arch}" + debian_dir="$stage/DEBIAN" + + mkdir -p "$stage/opt/v2rayN" "$stage/usr/bin" "$stage/usr/share/applications" "$stage/usr/share/icons/hicolor/256x256/apps" "$debian_dir" + cp -a "$pubdir/." "$stage/opt/v2rayN/" + + 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 + + stage_runtime_assets "$stage/opt/v2rayN" "$rid" + write_launcher_file "$stage" + write_desktop_file "$stage" + write_maintainer_scripts "$debian_dir" + + 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 + + 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 + + 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 + + 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 + + deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_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") +} + +select_targets() { + printf '%s\n' loongarch64 +} + +build_one_target() { + local short="$1" + local meta=() + local rid="" + local deb_arch="" + + mapfile -t meta < <(describe_target "$short") || return 1 + rid="${meta[0]}" + deb_arch="${meta[1]}" + + echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)" + publish_binary "$rid" + package_binary "$short" "$rid" "$deb_arch" +} + +print_summary() { + local pkg="" + + 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 "===============================================" +} + +main() { + local targets=() + local arch="" + + parse_args "$@" + detect_environment + install_dependencies + prepare_workspace + resolve_version + + mapfile -t targets < <(select_targets) + + for arch in "${targets[@]}"; do + build_one_target "$arch" + done + + print_summary +} + +main "$@" diff --git a/package-debian-riscv.sh b/package-debian-riscv.sh new file mode 100644 index 00000000000..e9fcb628a6d --- /dev/null +++ b/package-debian-riscv.sh @@ -0,0 +1,727 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION_ARG="" +WITH_CORE="both" +FORCE_NETCORE=0 +BUILD_FROM="" +XRAY_VER="${XRAY_VER:-}" +SING_VER="${SING_VER:-}" + +MIN_KERNEL="5.10" +PKGROOT="v2rayN-publish" +PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" +OUTPUT_DIR="${HOME}/debbuild" +DOTNET_RISCV_VERSION="10.0.108" +DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" +DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" +DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" + +OS_ID="" +OS_NAME="" +OS_VERSION_ID="" +HOST_ARCH="" +SCRIPT_DIR="" +PROJECT="" +VERSION="" + +declare -a BUILT_DEBS=() + +die() { + echo "$*" >&2 + exit 1 +} + +parse_args() { + local first_arg="${1:-}" + + if [[ -n "$first_arg" && "$first_arg" != --* ]]; then + VERSION_ARG="$first_arg" + shift || true + fi + + 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 ;; + --buildfrom) BUILD_FROM="${2:-}"; shift 2 ;; + *) + [[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1" + shift + ;; + esac + done + + if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then + die "You cannot specify both an explicit version and --buildfrom at the same time. + Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." + fi +} + +detect_environment() { + local current_kernel="" + local lowest="" + + . /etc/os-release + + OS_ID="${ID:-}" + OS_NAME="${NAME:-$OS_ID}" + OS_VERSION_ID="${VERSION_ID:-}" + HOST_ARCH="$(uname -m)" + + case "$OS_ID" in + debian) + echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}" + ;; + *) + die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}). +This script only supports: Debian." + ;; + esac + + case "$HOST_ARCH" in + riscv64) ;; + *) die "Only supports riscv64" ;; + esac + + current_kernel="$(uname -r)" + lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)" + + [[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL" + echo "[OK] Kernel $current_kernel verified." +} + +install_dependencies() { + local install_ok=0 + local tmp_dotnet="" + + mkdir -p "$OUTPUT_DIR" + + 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 gcc make pkg-config \ + libicu-dev libssl-dev libfontconfig1 libfreetype6 zlib1g + + mkdir -p "$HOME/.dotnet" + tmp_dotnet="$(mktemp -d)" + curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_RISCV_FILE" + tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_RISCV_FILE" + rm -rf "$tmp_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 '$OS_ID'. Make sure these are available:" + echo "dotnet-riscv SDK, curl, unzip, tar, rsync, git, gcc, make, dpkg-deb, fakeroot, libicu-dev, libssl-dev" + exit 1 + fi +} + +prepare_workspace() { + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + cd "$SCRIPT_DIR" + + if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true + fi + + PROJECT="$PROJECT_HINT" + [[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" + [[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found" +} + +choose_channel() { + local ch="latest" + local sel="" + + if [[ -n "${BUILD_FROM:-}" ]]; then + case "$BUILD_FROM" in + 1) echo "latest"; return 0 ;; + 2) echo "prerelease"; return 0 ;; + 3) echo "keep"; return 0 ;; + *) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;; + esac + fi + + 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 + git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want" + + if [[ -n "$ref" ]]; then + echo "[OK] Found ref '${ref}', checking out..." + git checkout -f "$ref" + sync_submodules + return 0 + fi + fi + + return 1 +} + +apply_channel_or_keep() { + local ch="$1" + local 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..." + + case "$ch" in + latest) tag="$(get_latest_tag_latest || true)" ;; + prerelease) tag="$(get_latest_tag_prerelease || true)" ;; + *) die "Failed to resolve latest tag for channel '${ch}'." ;; + esac + + [[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'." + + echo "[*] Latest tag for '${ch}': ${tag}" + git_try_checkout "$tag" || die "Failed to checkout '${tag}'." + VERSION="${tag#v}" +} + +resolve_version() { + if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + local clean_ver="${VERSION_ARG#v}" + + if git_try_checkout "$clean_ver"; then + VERSION="$clean_ver" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + apply_channel_or_keep "$(choose_channel)" + fi + else + apply_channel_or_keep "$(choose_channel)" + 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}" +} + +xray_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-riscv64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-riscv64.zip" ;; + *) return 1 ;; + esac +} + +singbox_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-riscv64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-riscv64.tar.gz" ;; + *) return 1 ;; + esac +} + +bundle_url_for_rid() { + local rid="$1" + + case "$rid" in + linux-riscv64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-riscv64.zip" ;; + *) return 1 ;; + esac +} + +download_xray() { + local outdir="$1" + local rid="$2" + local ver="${XRAY_VER:-}" + local url="" + local tmp="" + + 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; } + url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; } + + echo "[+] Download xray: $url" + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; } + unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; } + install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; } + rm -rf "$tmp" +} + +download_singbox() { + local outdir="$1" + local rid="$2" + local ver="${SING_VER:-}" + local url="" + local tmp="" + local bin="" + local cronet="" + + 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; } + url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; } + + echo "[+] Download sing-box: $url" + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + + 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"; return 1; } + + cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)" + [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true + + rm -rf "$tmp" +} + +unify_geo_layout() { + local outroot="$1" + local n + local names=( + geosite.dat + geoip.dat + geoip-only-cn-private.dat + Country.mmdb + geoip.metadb + ) + + mkdir -p "$outroot/bin" + + 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" + local f="" + + 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" + + 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/refs/heads/rule-set-geoip/$f" + 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/refs/heads/rule-set-geosite/$f" + done + + unify_geo_layout "$outroot" +} + +populate_assets_zip_mode() { + local outroot="$1" + local rid="$2" + local url="" + local tmp="" + local nested_dir="" + + url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; } + + echo "[+] Try v2rayN bundle archive: $url" + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; } + unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; 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 + + 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_geo_layout "$outroot" + rm -rf "$tmp" + + echo "[+] Bundle extracted to $outroot" +} + +populate_assets_netcore_mode() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" + + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)" + fi + + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)" + fi + + download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" +} + +stage_runtime_assets() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" + + if [[ "$FORCE_NETCORE" -eq 0 ]]; then + if populate_assets_zip_mode "$outroot" "$rid"; then + echo "[*] Using v2rayN bundle archive." + else + echo "[*] Bundle failed, fallback to separate core + rules." + populate_assets_netcore_mode "$outroot" "$rid" + fi + else + echo "[*] --netcore specified: use separate core + rules." + populate_assets_netcore_mode "$outroot" "$rid" + fi +} + +describe_target() { + local short="$1" + + case "$short" in + riscv64) printf '%s\n%s\n' "linux-riscv64" "riscv64" ;; + *) echo "Unknown arch '$short' (use riscv64)" >&2; return 1 ;; + esac +} + +publish_binary() { + local rid="$1" + + dotnet clean "$PROJECT" -c Release + rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true + dotnet restore "$PROJECT" + dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true +} + +write_launcher_file() { + local stage="$1" + + 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 +} + +write_desktop_file() { + local stage="$1" + + install -m 644 /dev/stdin "$stage/usr/share/applications/v2rayn.desktop" <<'EOF' +[Desktop Entry] +Type=Application +Name=v2rayN +Comment=v2rayN for Debian GNU Linux +Exec=v2rayn +Icon=v2rayn +Terminal=false +Categories=Network; +EOF +} + +write_maintainer_scripts() { + local debian_dir="$1" + + install -m 755 /dev/stdin "$debian_dir/postinst" <<'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 + + 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 +} + +package_binary() { + local short="$1" + local rid="$2" + local deb_arch="$3" + local pubdir="" + local workdir="" + local stage="" + local debian_dir="" + local project_dir="" + local icon_candidate="" + local shlibs_depends="" + local extra_depends="" + local final_depends="" + local multiarch="" + local sys_libdir="" + local sys_usrlibdir="" + local deb_out="" + + pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish" + [[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; } + + workdir="$(mktemp -d)" + trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN + + stage="$workdir/${PKGROOT}_${VERSION}_${deb_arch}" + debian_dir="$stage/DEBIAN" + + mkdir -p "$stage/opt/v2rayN" "$stage/usr/bin" "$stage/usr/share/applications" "$stage/usr/share/icons/hicolor/256x256/apps" "$debian_dir" + cp -a "$pubdir/." "$stage/opt/v2rayN/" + + 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 + + stage_runtime_assets "$stage/opt/v2rayN" "$rid" + write_launcher_file "$stage" + write_desktop_file "$stage" + write_maintainer_scripts "$debian_dir" + + 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 + + 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 + + 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 + + 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 + + deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_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") +} + +select_targets() { + printf '%s\n' riscv64 +} + +build_one_target() { + local short="$1" + local meta=() + local rid="" + local deb_arch="" + + mapfile -t meta < <(describe_target "$short") || return 1 + rid="${meta[0]}" + deb_arch="${meta[1]}" + + echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)" + publish_binary "$rid" + package_binary "$short" "$rid" "$deb_arch" +} + +print_summary() { + local pkg="" + + 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 "===============================================" +} + +main() { + local targets=() + local arch="" + + parse_args "$@" + detect_environment + install_dependencies + prepare_workspace + resolve_version + + mapfile -t targets < <(select_targets) + + for arch in "${targets[@]}"; do + build_one_target "$arch" + done + + print_summary +} + +main "$@" diff --git a/package-debian.sh b/package-debian.sh index 32e9e6201d6..0c10a548492 100644 --- a/package-debian.sh +++ b/package-debian.sh @@ -1,141 +1,167 @@ #!/usr/bin/env bash set -euo pipefail -# Require Debian base branch -. /etc/os-release - -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 +VERSION_ARG="" +WITH_CORE="both" +FORCE_NETCORE=0 +ARCH_OVERRIDE="" +BUILD_FROM="" +XRAY_VER="${XRAY_VER:-}" +SING_VER="${SING_VER:-}" + +MIN_KERNEL="6.12" +PKGROOT="v2rayN-publish" +PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" +OUTPUT_DIR="${HOME}/debbuild" + +OS_ID="" +OS_NAME="" +OS_VERSION_ID="" +HOST_ARCH="" +SCRIPT_DIR="" +PROJECT="" +VERSION="" + +declare -a BUILT_DEBS=() + +die() { + echo "$*" >&2 + exit 1 +} -# Kernel version -MIN_KERNEL="6.11" -CURRENT_KERNEL="$(uname -r)" +parse_args() { + local first_arg="${1:-}" -lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$CURRENT_KERNEL" | sort -V | head -n1)" + if [[ -n "$first_arg" && "$first_arg" != --* ]]; then + VERSION_ARG="$first_arg" + shift || true + fi -if [[ "$lowest" != "$MIN_KERNEL" ]]; then - echo "Kernel $CURRENT_KERNEL is below $MIN_KERNEL" - exit 1 -fi + 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 ;; + *) + [[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1" + shift + ;; + esac + done -echo "[OK] Kernel $CURRENT_KERNEL verified." + if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then + die "You cannot specify both an explicit version and --buildfrom at the same time. + Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." + fi +} -# Config & Parse arguments -VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty -WITH_CORE="both" # Default: bundle both xray+sing-box -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 +detect_environment() { + local current_kernel="" + local lowest="" -# 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;; + . /etc/os-release + + OS_ID="${ID:-}" + OS_NAME="${NAME:-$OS_ID}" + OS_VERSION_ID="${VERSION_ID:-}" + HOST_ARCH="$(uname -m)" + + case "$OS_ID" in + debian) + echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}" + ;; *) - if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi - shift;; + die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}). +This script only supports: Debian." + ;; 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 + case "$HOST_ARCH" in + x86_64|aarch64) ;; + *) die "Only supports aarch64 / x86_64" ;; + esac -# Check and install dependencies -host_arch="$(uname -m)" -[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; } + current_kernel="$(uname -r)" + lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)" -install_ok=0 + [[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL" + echo "[OK] Kernel $current_kernel verified." +} -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 +install_dependencies() { + local install_ok=0 + local foreign_arch="" - if [[ "$host_arch" == "aarch64" ]]; then - sudo dpkg --add-architecture amd64 || true + mkdir -p "$OUTPUT_DIR" + + if command -v apt-get >/dev/null 2>&1; then 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 + curl unzip tar jq rsync ca-certificates git dpkg-dev fakeroot file \ + desktop-file-utils xdg-utils wget + + case "$HOST_ARCH" in + aarch64) foreign_arch="amd64" ;; + x86_64) foreign_arch="arm64" ;; + *) die "Only supports aarch64 / x86_64" ;; + esac + + sudo dpkg --add-architecture "$foreign_arch" || true sudo apt-get update sudo apt-get -y install \ - libc6:arm64 libgcc-s1:arm64 libstdc++6:arm64 zlib1g:arm64 libfontconfig1:arm64 - fi + "libc6:${foreign_arch}" \ + "libgcc-s1:${foreign_arch}" \ + "libstdc++6:${foreign_arch}" \ + "zlib1g:${foreign_arch}" \ + "libfontconfig1:${foreign_arch}" - # 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" + wget -q https://dot.net/v1/dotnet-install.sh + chmod +x dotnet-install.sh + ./dotnet-install.sh --channel 10.0.1xx --install-dir "$HOME/.dotnet" - export PATH="$HOME/.dotnet:$PATH" - export DOTNET_ROOT="$HOME/.dotnet" + export PATH="$HOME/.dotnet:$PATH" + export DOTNET_ROOT="$HOME/.dotnet" - dotnet --info >/dev/null 2>&1 && install_ok=1 -fi + 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 + if [[ "$install_ok" -ne 1 ]]; then + echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:" + echo "dotnet-sdk 10.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" +prepare_workspace() { + 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 + 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; } + PROJECT="$PROJECT_HINT" + [[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" + [[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found" +} choose_channel() { - # If --buildfrom provided, map it directly and skip interaction. + local ch="latest" + local sel="" + 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;; + 1) echo "latest"; return 0 ;; + 2) echo "prerelease"; return 0 ;; + 3) echo "keep"; return 0 ;; + *) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;; 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 @@ -166,28 +192,35 @@ get_latest_tag_prerelease() { | sed 's/^v//' } +sync_submodules() { + if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true + fi +} + git_try_checkout() { - local want="$1" ref="" + local want="$1" + local ref="" + if git rev-parse --git-dir >/dev/null 2>&1; then git fetch --tags --force --prune --depth=1 || true - if git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then - ref="${want}" - fi + git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want" + 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 + git checkout -f "$ref" + sync_submodules return 0 fi fi + return 1 } apply_channel_or_keep() { - local ch="$1" tag + local ch="$1" + local tag="" if [[ "$ch" == "keep" ]]; then echo "[*] Keep current repository state (no checkout)." @@ -197,99 +230,154 @@ apply_channel_or_keep() { 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; } + case "$ch" in + latest) tag="$(get_latest_tag_latest || true)" ;; + prerelease) tag="$(get_latest_tag_prerelease || true)" ;; + *) die "Failed to resolve latest tag for channel '${ch}'." ;; + esac + + [[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'." + echo "[*] Latest tag for '${ch}': ${tag}" - git_try_checkout "$tag" || { echo "Failed to checkout '${tag}'."; exit 1; } + git_try_checkout "$tag" || die "Failed to checkout '${tag}'." 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" +resolve_version() { + if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + local clean_ver="${VERSION_ARG#v}" + + if git_try_checkout "$clean_ver"; then + VERSION="$clean_ver" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + apply_channel_or_keep "$(choose_channel)" + fi else - echo "[WARN] Tag '${VERSION_ARG}' not found." - ch="$(choose_channel)" - apply_channel_or_keep "$ch" + apply_channel_or_keep "$(choose_channel)" fi else - ch="$(choose_channel)" - apply_channel_or_keep "$ch" + echo "Current directory is not a git repo; proceeding on current tree." + VERSION="${VERSION_ARG:-0.0.0}" 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}" + VERSION="${VERSION#v}" + echo "[*] GUI version resolved as: ${VERSION}" +} + +xray_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-x64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip" ;; + linux-arm64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip" ;; + *) return 1 ;; + esac +} + +singbox_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-x64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz" ;; + linux-arm64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz" ;; + *) return 1 ;; + esac +} + +bundle_url_for_rid() { + local rid="$1" + + case "$rid" in + linux-x64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip" ;; + linux-arm64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip" ;; + *) return 1 ;; + esac +} download_xray() { - local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" + local outdir="$1" + local rid="$2" + local ver="${XRAY_VER:-}" + local url="" + local tmp="" + 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 + | 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 + url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; } + 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" + curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; } + unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; } + install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; } rm -rf "$tmp" } download_singbox() { - # Download sing-box - local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin cronet + local outdir="$1" + local rid="$2" + local ver="${SING_VER:-}" + local url="" + local tmp="" + local bin="" + local cronet="" + 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 + url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; } + echo "[+] Download sing-box: $url" + tmp="$(mktemp -d)" - curl -fL "$url" -o "$tmp/$tarname" - tar -C "$tmp" -xzf "$tmp/$tarname" + curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + 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" + + install -m 755 "$bin" "$outdir/sing-box" || { rm -rf "$tmp"; return 1; } + cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)" - [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" + [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true + rm -rf "$tmp" } unify_geo_layout() { local outroot="$1" - mkdir -p "$outroot/bin" + local n local names=( - "geosite.dat" - "geoip.dat" - "geoip-only-cn-private.dat" - "Country.mmdb" - "geoip.metadb" + geosite.dat + geoip.dat + geoip-only-cn-private.dat + Country.mmdb + geoip.metadb ) + + mkdir -p "$outroot/bin" + for n in "${names[@]}"; do if [[ -f "$outroot/bin/xray/$n" ]]; then mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n" @@ -301,52 +389,44 @@ download_geo_assets() { local outroot="$1" local bin_dir="$outroot/bin" local srss_dir="$bin_dir/srss" + local f="" + 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" + 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 + curl -fsSL -o "$bin_dir/geoip.metadb" "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" + + 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/refs/heads/rule-set-geoip/$f" 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 + 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/refs/heads/rule-set-geosite/$f" done unify_geo_layout "$outroot" } -download_v2rayn_bundle() { - local outroot="$1" rid="$2" +populate_assets_zip_mode() { + local outroot="$1" + local 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 + local tmp="" + local nested_dir="" + + url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; } + 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; } + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; } + unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; return 1; } if [[ -d "$tmp/bin" ]]; then mkdir -p "$outroot/bin" @@ -358,7 +438,6 @@ download_v2rayn_bundle() { 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" @@ -366,90 +445,71 @@ download_v2rayn_bundle() { rm -rf "$nested_dir" fi - # Unify to bin/ unify_geo_layout "$outroot" + rm -rf "$tmp" echo "[+] Bundle extracted to $outroot" } -BUILT_DEBS=() -BUILT_ALL=0 -OUTPUT_DIR="$HOME/debbuild" -mkdir -p "$OUTPUT_DIR" +populate_assets_netcore_mode() { + local outroot="$1" + local rid="$2" -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 + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" - echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)" + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)" + fi - dotnet clean "$PROJECT" -c Release - rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)" + fi - 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)" - } + download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" +} + +stage_runtime_assets() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" if [[ "$FORCE_NETCORE" -eq 0 ]]; then - if download_v2rayn_bundle "$STAGE/opt/v2rayN" "$RID_DIR"; then + if populate_assets_zip_mode "$outroot" "$rid"; then echo "[*] Using v2rayN bundle bin assets." else echo "[*] Bundle failed, fallback to separate core + rules." - fetch_separate_cores_and_rules "$STAGE/opt/v2rayN" + populate_assets_netcore_mode "$outroot" "$rid" fi else echo "[*] --netcore specified: use separate core + rules." - fetch_separate_cores_and_rules "$STAGE/opt/v2rayN" + populate_assets_netcore_mode "$outroot" "$rid" fi +} - # Wrapper - install -m 755 /dev/stdin "$STAGE/usr/bin/v2rayn" <<'EOF' +describe_target() { + local short="$1" + + case "$short" in + x64) printf '%s\n%s\n' "linux-x64" "amd64" ;; + arm64) printf '%s\n%s\n' "linux-arm64" "arm64" ;; + *) echo "Unknown arch '$short' (use x64|arm64)" >&2; return 1 ;; + esac +} + +publish_binary() { + local rid="$1" + + dotnet clean "$PROJECT" -c Release + rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true + dotnet restore "$PROJECT" + dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true +} + +write_launcher_file() { + local stage="$1" + + install -m 755 /dev/stdin "$stage/usr/bin/v2rayn" <<'EOF' #!/usr/bin/env bash set -euo pipefail DIR="/opt/v2rayN" @@ -469,12 +529,90 @@ echo "v2rayN launcher: no executable found in $DIR" >&2 ls -l "$DIR" >&2 || true exit 1 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)" +write_desktop_file() { + local stage="$1" + + install -m 644 /dev/stdin "$stage/usr/share/applications/v2rayn.desktop" <<'EOF' +[Desktop Entry] +Type=Application +Name=v2rayN +Comment=v2rayN for Debian GNU Linux +Exec=v2rayn +Icon=v2rayn +Terminal=false +Categories=Network; +EOF +} + +write_maintainer_scripts() { + local debian_dir="$1" + + install -m 755 /dev/stdin "$debian_dir/postinst" <<'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 + + 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 +} - mkdir -p "$WORKDIR/debian" - cat > "$WORKDIR/debian/control" < "$workdir/debian/control" < "$debian_dir/substvars" - : > "$DEBIAN_DIR/substvars" mapfile -t ELF_FILES < <( - find "$STAGE/opt/v2rayN" -type f \( -name "*.so*" -o -perm -111 \) ! -name 'libcoreclrtraceptprovider.so' + find "$stage/opt/v2rayN" -type f \( -name "*.so*" -o -perm -111 \) ! -name 'libcoreclrtraceptprovider.so' ) + if [[ "${#ELF_FILES[@]}" -gt 0 ]]; then ( - cd "$WORKDIR" + cd "$workdir" dpkg-shlibdeps \ - -l"$STAGE/opt/v2rayN" \ - -l"$SYS_LIBDIR" \ - -l"$SYS_USRLIBDIR" \ - -T"$DEBIAN_DIR/substvars" \ + -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)" + shlibs_depends="$(sed -n 's/^shlibs:Depends=//p' "$debian_dir/substvars" | head -n1 || true)" - if [[ -n "$SHLIBS_DEPENDS" ]]; then - SHLIBS_DEPENDS="$(echo "$SHLIBS_DEPENDS" \ + 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}" + final_depends="${shlibs_depends}, ${extra_depends}" else - FINAL_DEPENDS="${EXTRA_DEPENDS}" + final_depends="${extra_depends}" fi - # Desktop file - install -m 644 /dev/stdin "$STAGE/usr/share/applications/v2rayn.desktop" <<'EOF' -[Desktop Entry] -Type=Application -Name=v2rayN -Comment=v2rayN for Debian GNU Linux -Exec=v2rayn -Icon=v2rayn -Terminal=false -Categories=Network; -EOF - - # Control file - cat > "$DEBIAN_DIR/control" < "$debian_dir/control" < Homepage: https://github.com/2dust/v2rayN Section: net Priority: optional -Depends: ${FINAL_DEPENDS} +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 /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 + 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 - # 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 - - # 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" - - dpkg-deb --root-owner-group --build "$STAGE" "$deb_out" + 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" +select_targets() { + case "${ARCH_OVERRIDE:-}" in + all) printf '%s\n' x64 arm64 ;; + x64|amd64) printf '%s\n' x64 ;; + arm64|aarch64) printf '%s\n' arm64 ;; + "") + case "$HOST_ARCH" in + x86_64) printf '%s\n' x64 ;; + aarch64) printf '%s\n' arm64 ;; + *) return 1 ;; + esac + ;; + *) + echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all." >&2 + return 1 + ;; + esac } -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 +build_one_target() { + local short="$1" + local meta=() + local rid="" + local deb_arch="" + + mapfile -t meta < <(describe_target "$short") || return 1 + rid="${meta[0]}" + deb_arch="${meta[1]}" -for arch in "${targets[@]}"; do - build_for_arch "$arch" -done + echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)" + publish_binary "$rid" + package_binary "$short" "$rid" "$deb_arch" +} + +print_summary() { + local pkg="" + + 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 "===============================================" +} + +main() { + local targets=() + local arch="" -echo "" -echo "================ Build Summary =================" -if [[ "${#BUILT_DEBS[@]}" -gt 0 ]]; then - echo "Output directory: $OUTPUT_DIR" - for pkg in "${BUILT_DEBS[@]}"; do - echo "$pkg" + parse_args "$@" + detect_environment + install_dependencies + prepare_workspace + resolve_version + + mapfile -t targets < <(select_targets) + + for arch in "${targets[@]}"; do + build_one_target "$arch" done -else - echo "No DEBs detected in summary (check build logs above)." -fi -echo "===============================================" + + print_summary +} + +main "$@" \ No newline at end of file diff --git a/package-osx.sh b/package-osx.sh index 5ed390ca903..4eb14366a35 100755 --- a/package-osx.sh +++ b/package-osx.sh @@ -22,7 +22,17 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF CFBundleDevelopmentRegion - English + en + CFBundleLocalizations + + zh-Hans + zh-Hant + en + fa + fr + ru + hu + CFBundleDisplayName v2rayN CFBundleExecutable diff --git a/package-rhel-riscv.sh b/package-rhel-riscv.sh index ef22934c383..9c4fa03fb27 100644 --- a/package-rhel-riscv.sh +++ b/package-rhel-riscv.sh @@ -1,232 +1,154 @@ #!/usr/bin/env bash set -euo pipefail -# Require Red Hat base branch -. /etc/os-release - -case "${ID:-}" in - rhel|rocky|almalinux|fedora|centos) - echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}" - ;; - *) - echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})." - echo "This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS." - exit 1 - ;; -esac +VERSION_ARG="" +WITH_CORE="both" +FORCE_NETCORE=0 +BUILD_FROM="" +XRAY_VER="${XRAY_VER:-}" +SING_VER="${SING_VER:-}" -# Kernel version MIN_KERNEL="5.10" -CURRENT_KERNEL="$(uname -r)" - -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 "[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 -BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively -DOTNET_RISCV_VERSION="10.0.105" -DOTNET_RISCV_BASE="https://github.com/filipnavara/dotnet-riscv/releases/download" +PKGROOT="v2rayN-publish" +PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" +RPM_TOPDIR="${HOME}/rpmbuild" +DOTNET_RISCV_VERSION="10.0.108" +DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" -SKIA_VER="${SKIA_VER:-3.119.2}" -HARFBUZZ_VER="${HARFBUZZ_VER:-8.3.1.1}" - -# 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;; - --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 - -apply_riscv_patch() { - # Upgrade net8.0 -> net10.0 - find . -type f \( -name "*.csproj" -o -name "*.props" -o -name "*.targets" \) \ - -exec sed -i 's/net8\.0/net10.0/g' {} + - - # Patch all Directory.Packages.props for SkiaSharp/HarfBuzzSharp - while IFS= read -r -d '' f; do - # replace existing versions if present - sed -i \ - -e "s###g" \ - -e "s###g" \ - -e "s###g" \ - -e "s###g" \ - "$f" - - grep -q 'PackageVersion Include="SkiaSharp"' "$f" || \ - sed -i "/<\/ItemGroup>/i\ " "$f" - - grep -q 'PackageVersion Include="SkiaSharp.NativeAssets.Linux"' "$f" || \ - sed -i "/<\/ItemGroup>/i\ " "$f" - - grep -q 'PackageVersion Include="HarfBuzzSharp"' "$f" || \ - sed -i "/<\/ItemGroup>/i\ " "$f" - - grep -q 'PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux"' "$f" || \ - sed -i "/<\/ItemGroup>/i\ " "$f" - done < <(find . -type f -name 'Directory.Packages.props' -print0) - - # Patch SDK bundled RIDs - f="$(find "$DOTNET_ROOT/sdk/$(dotnet --version)" -type f -name 'Microsoft.NETCoreSdk.BundledVersions.props' | head -n1 || true)" - [[ -f "$f" ]] && sed -i \ - -e 's/linux-arm64/&;linux-riscv64/g' \ - -e 's/linux-musl-arm64/&;linux-musl-riscv64/g' \ - "$f" -} +OS_ID="" +OS_NAME="" +OS_VERSION_ID="" +HOST_ARCH="" +SCRIPT_DIR="" +PROJECT="" +VERSION="" -build_sqlite_native_riscv64() { - local outdir="$1" - local workdir sqlite_year sqlite_ver sqlite_zip srcdir +declare -a BUILT_RPMS=() - mkdir -p "$outdir" - workdir="$(mktemp -d)" - - # SQLite 3.51.3 amalgamation - sqlite_year="2026" - sqlite_ver="3510300" - sqlite_zip="sqlite-amalgamation-${sqlite_ver}.zip" +die() { + echo "$*" >&2 + exit 1 +} - echo "[+] Download SQLite amalgamation: ${sqlite_zip}" - curl -fL "https://www.sqlite.org/${sqlite_year}/${sqlite_zip}" -o "${workdir}/${sqlite_zip}" +parse_args() { + local first_arg="${1:-}" - unzip -q "${workdir}/${sqlite_zip}" -d "$workdir" - srcdir="$(find "$workdir" -maxdepth 1 -type d -name 'sqlite-amalgamation-*' | head -n1 || true)" - [[ -n "$srcdir" ]] || { echo "[!] SQLite source unpack failed"; rm -rf "$workdir"; return 1; } + if [[ -n "$first_arg" && "$first_arg" != --* ]]; then + VERSION_ARG="$first_arg" + shift || true + fi - echo "[+] Build libe_sqlite3.so for riscv64" - gcc -shared -fPIC -O2 \ - -DSQLITE_THREADSAFE=1 \ - -DSQLITE_ENABLE_FTS5 \ - -DSQLITE_ENABLE_RTREE \ - -DSQLITE_ENABLE_JSON1 \ - -o "${outdir}/libe_sqlite3.so" "${srcdir}/sqlite3.c" -ldl -lpthread + 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 ;; + --buildfrom) BUILD_FROM="${2:-}"; shift 2 ;; + *) + [[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1" + shift + ;; + esac + done - rm -rf "$workdir" + if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then + die "You cannot specify both an explicit version and --buildfrom at the same time. + Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." + fi } -copy_skiasharp_native_riscv64() { - local outdir="$1" - local skia_so="" - local harfbuzz_so="" +detect_environment() { + local current_kernel="" + local lowest="" - mkdir -p "$outdir" + . /etc/os-release - skia_so="$(find "$HOME/.nuget/packages" -path "*/skiasharp.nativeassets.linux/${SKIA_VER}/runtimes/linux-riscv64/native/libSkiaSharp.so" | head -n1 || true)" - if [[ -z "$skia_so" ]]; then - skia_so="$(find "$HOME/.nuget/packages" -path "*/runtimes/linux-riscv64/native/libSkiaSharp.so" | head -n1 || true)" - fi + OS_ID="${ID:-}" + OS_NAME="${NAME:-$OS_ID}" + OS_VERSION_ID="${VERSION_ID:-}" + HOST_ARCH="$(uname -m)" - harfbuzz_so="$(find "$HOME/.nuget/packages" -path "*/harfbuzzsharp.nativeassets.linux/${HARFBUZZ_VER}/runtimes/linux-riscv64/native/libHarfBuzzSharp.so" | head -n1 || true)" - if [[ -z "$harfbuzz_so" ]]; then - harfbuzz_so="$(find "$HOME/.nuget/packages" -path "*/runtimes/linux-riscv64/native/libHarfBuzzSharp.so" | head -n1 || true)" - fi + case "$OS_ID" in + rhel|rocky|almalinux|fedora|centos) + echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}" + ;; + *) + die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}). +This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS." + ;; + esac - if [[ -n "$skia_so" && -f "$skia_so" ]]; then - echo "[+] Copy libSkiaSharp.so from NuGet cache" - install -m 755 "$skia_so" "$outdir/libSkiaSharp.so" - else - echo "[WARN] libSkiaSharp.so for linux-riscv64 not found in NuGet cache" - fi + case "$HOST_ARCH" in + riscv64) ;; + *) die "Only supports riscv64" ;; + esac - if [[ -n "$harfbuzz_so" && -f "$harfbuzz_so" ]]; then - echo "[+] Copy libHarfBuzzSharp.so from NuGet cache" - install -m 755 "$harfbuzz_so" "$outdir/libHarfBuzzSharp.so" - else - echo "[WARN] libHarfBuzzSharp.so for linux-riscv64 not found in NuGet cache" - fi -} + current_kernel="$(uname -r)" + lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)" -# Check and install dependencies -host_arch="$(uname -m)" -[[ "$host_arch" == "riscv64" ]] || { echo "Only supports riscv64"; exit 1; } + [[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL" + echo "[OK] Kernel $current_kernel verified." +} -install_ok=0 +install_dependencies() { + local install_ok=0 + local tmp_dotnet="" -if command -v dnf >/dev/null 2>&1; then - sudo dnf -y install \ - rpm-build rpmdevtools curl unzip tar jq rsync git python3 gcc make \ - glibc-devel kernel-headers libatomic file ca-certificates libicu\ - && install_ok=1 + if command -v dnf >/dev/null 2>&1; then + sudo dnf -y install \ + rpm-build rpmdevtools curl unzip tar jq rsync git python3 \ + glibc-devel kernel-headers libatomic file ca-certificates libicu \ + && install_ok=1 - mkdir -p "$HOME/.dotnet" - tmp_dotnet="$(mktemp -d)" - curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_RISCV_FILE" - tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_RISCV_FILE" - rm -rf "$tmp_dotnet" + mkdir -p "$HOME/.dotnet" + tmp_dotnet="$(mktemp -d)" + curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_RISCV_FILE" + tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_RISCV_FILE" + rm -rf "$tmp_dotnet" - export PATH="$HOME/.dotnet:$PATH" - export DOTNET_ROOT="$HOME/.dotnet" + export PATH="$HOME/.dotnet:$PATH" + export DOTNET_ROOT="$HOME/.dotnet" - dotnet --info >/dev/null 2>&1 || install_ok=0 -fi + dotnet --info >/dev/null 2>&1 || install_ok=0 + fi -if [[ "$install_ok" -ne 1 ]]; then - echo "Could not auto-install dependencies for '$ID'. Make sure these are available:" - echo "dotnet-riscv SDK, curl, unzip, tar, rsync, git, python3, gcc, rpm, rpmdevtools, rpm-build (on Red Hat branch)" - exit 1 -fi + if [[ "$install_ok" -ne 1 ]]; then + echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:" + echo "dotnet-riscv SDK, curl, unzip, tar, rsync, git, python3, rpm, rpmdevtools, rpm-build (on Red Hat branch)" + exit 1 + fi +} -# Root directory -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" +prepare_workspace() { + 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 + 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; } + PROJECT="$PROJECT_HINT" + [[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" + [[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found" +} choose_channel() { - # If --buildfrom provided, map it directly and skip interaction. + local ch="latest" + local sel="" + 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;; + 1) echo "latest"; return 0 ;; + 2) echo "prerelease"; return 0 ;; + 3) echo "keep"; return 0 ;; + *) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;; 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 @@ -257,29 +179,35 @@ get_latest_tag_prerelease() { | sed 's/^v//' } +sync_submodules() { + if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true + fi +} + git_try_checkout() { - # Try a series of refs and checkout when found. - local want="$1" ref="" + local want="$1" + local ref="" + if git rev-parse --git-dir >/dev/null 2>&1; then git fetch --tags --force --prune --depth=1 || true - if git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then - ref="${want}" - fi + git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want" + 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 + git checkout -f "$ref" + sync_submodules return 0 fi fi + return 1 } apply_channel_or_keep() { - local ch="$1" tag + local ch="$1" + local tag="" if [[ "$ch" == "keep" ]]; then echo "[*] Keep current repository state (no checkout)." @@ -289,103 +217,151 @@ apply_channel_or_keep() { 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; } + case "$ch" in + latest) tag="$(get_latest_tag_latest || true)" ;; + prerelease) tag="$(get_latest_tag_prerelease || true)" ;; + *) die "Failed to resolve latest tag for channel '${ch}'." ;; + esac + + [[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'." + echo "[*] Latest tag for '${ch}': ${tag}" - git_try_checkout "$tag" || { echo "Failed to checkout '${tag}'."; exit 1; } + git_try_checkout "$tag" || die "Failed to checkout '${tag}'." 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" +resolve_version() { + if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + local clean_ver="${VERSION_ARG#v}" + + if git_try_checkout "$clean_ver"; then + VERSION="$clean_ver" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + apply_channel_or_keep "$(choose_channel)" + fi else - echo "[WARN] Tag '${VERSION_ARG}' not found." - ch="$(choose_channel)" - apply_channel_or_keep "$ch" + apply_channel_or_keep "$(choose_channel)" fi else - ch="$(choose_channel)" - apply_channel_or_keep "$ch" + echo "Current directory is not a git repo; proceeding on current tree." + VERSION="${VERSION_ARG:-0.0.0}" 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}" + VERSION="${VERSION#v}" + echo "[*] GUI version resolved as: ${VERSION}" +} + +xray_url_for_rid() { + local rid="$1" + local ver="$2" -# riscv64 patch -apply_riscv_patch + case "$rid" in + linux-riscv64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-riscv64.zip" ;; + *) return 1 ;; + esac +} + +singbox_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-riscv64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-riscv64.tar.gz" ;; + *) return 1 ;; + esac +} + +bundle_url_for_rid() { + local rid="$1" + + case "$rid" in + linux-riscv64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-riscv64.zip" ;; + *) return 1 ;; + esac +} -# Helpers for core download_xray() { - # Download Xray core - local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url="" tmp zipname="xray.zip" + local outdir="$1" + local rid="$2" + local ver="${XRAY_VER:-}" + local url="" + local tmp="" + 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 + | 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-riscv64" ]]; then - url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-riscv64.zip" - fi - [[ -n "$url" ]] || { echo "[xray] Unsupported RID: $rid"; return 1; } + url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; } + 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" + curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; } + unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; } + install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; } rm -rf "$tmp" } download_singbox() { - # Download sing-box - local outdir="$1" rid="$2" ver="${SING_VER:-}" url="" tmp tarname="singbox.tar.gz" bin cronet + local outdir="$1" + local rid="$2" + local ver="${SING_VER:-}" + local url="" + local tmp="" + local bin="" + local cronet="" + 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-riscv64" ]]; then - url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-riscv64.tar.gz" - fi - [[ -n "$url" ]] || { echo "[sing-box] Unsupported RID: $rid"; return 1; } + url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; } + echo "[+] Download sing-box: $url" + tmp="$(mktemp -d)" - curl -fL "$url" -o "$tmp/$tarname" - tar -C "$tmp" -xzf "$tmp/$tarname" + curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + 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" + + install -m 755 "$bin" "$outdir/sing-box" || { rm -rf "$tmp"; return 1; } + cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)" - [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" + [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true + rm -rf "$tmp" } -# Move geo files to outroot/bin unify_geo_layout() { local outroot="$1" - mkdir -p "$outroot/bin" - local names=( \ - "geosite.dat" \ - "geoip.dat" \ - "geoip-only-cn-private.dat" \ - "Country.mmdb" \ - "geoip.metadb" \ + local n + local names=( + geosite.dat + geoip.dat + geoip-only-cn-private.dat + Country.mmdb + geoip.metadb ) + + mkdir -p "$outroot/bin" + for n in "${names[@]}"; do if [[ -f "$outroot/bin/xray/$n" ]]; then mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n" @@ -393,57 +369,48 @@ unify_geo_layout() { done } -# Download geo/rule assets download_geo_assets() { local outroot="$1" local bin_dir="$outroot/bin" local srss_dir="$bin_dir/srss" + local f="" + 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" + 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 + curl -fsSL -o "$bin_dir/geoip.metadb" "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" + + 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/refs/heads/rule-set-geoip/$f" 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 + + 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/refs/heads/rule-set-geosite/$f" done - # Unify to bin unify_geo_layout "$outroot" } -# Prefer the prebuilt v2rayN core bundle; then unify geo layout -download_v2rayn_bundle() { - local outroot="$1" rid="$2" +populate_assets_zip_mode() { + local outroot="$1" + local rid="$2" local url="" - if [[ "$rid" == "linux-riscv64" ]]; then - url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-riscv64.zip" - fi - [[ -n "$url" ]] || { echo "[!] Bundle unsupported RID: $rid"; return 1; } + local tmp="" + local nested_dir="" + + url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; } + 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; } + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; } + unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; return 1; } if [[ -d "$tmp/bin" ]]; then mkdir -p "$outroot/bin" @@ -455,7 +422,6 @@ download_v2rayn_bundle() { 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" @@ -463,111 +429,73 @@ download_v2rayn_bundle() { rm -rf "$nested_dir" fi - # Unify to bin/ unify_geo_layout "$outroot" + rm -rf "$tmp" echo "[+] Bundle extracted to $outroot" } -# ===== Build results collection ======================================================== -BUILT_RPMS=() - -# ===== Build (single-arch) function ==================================================== -build_for_arch() { - # $1: target short arch: riscv64 - local short="$1" - local rid rpm_target archdir - case "$short" in - riscv64) rid="linux-riscv64"; rpm_target="riscv64"; archdir="riscv64" ;; - *) echo "Unknown arch '$short' (use riscv64)"; return 1;; - esac - - echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" - - # .NET publish (self-contained) for this RID - dotnet clean "$PROJECT" -c Release -p:TargetFramework=net10.0 - rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true - - dotnet restore "$PROJECT" -r "$rid" -p:TargetFramework=net10.0 - dotnet publish "$PROJECT" \ - -c Release -r "$rid" \ - -p:TargetFramework=net10.0 \ - -p:PublishSingleFile=false \ - -p:SelfContained=true - - # Per-arch variables (scoped) - local RID_DIR="$rid" - local PUBDIR - PUBDIR="$(dirname "$PROJECT")/bin/Release/net10.0/${RID_DIR}/publish" - [[ -d "$PUBDIR" ]] || { echo "Publish directory not found: $PUBDIR"; return 1; } - - # Per-arch working area - local PKGROOT="v2rayN-publish" - local WORKDIR - WORKDIR="$(mktemp -d)" - trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN - - # rpmbuild topdir selection - local TOPDIR SPECDIR SOURCEDIR PROJECT_DIR - rpmdev-setuptree - TOPDIR="${HOME}/rpmbuild" - SPECDIR="${TOPDIR}/SPECS" - SOURCEDIR="${TOPDIR}/SOURCES" +populate_assets_netcore_mode() { + local outroot="$1" + local rid="$2" - # Stage publish content - mkdir -p "$WORKDIR/$PKGROOT" - cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/" + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" - copy_skiasharp_native_riscv64 "$WORKDIR/$PKGROOT" || echo "[!] SkiaSharp native copy failed (skipped)" - build_sqlite_native_riscv64 "$WORKDIR/$PKGROOT" || echo "[!] sqlite native build failed (skipped)" + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)" + fi - # Required icon - local ICON_CANDIDATE - 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" + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)" + fi - # Prepare bin structure - mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box" + download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" +} - # Bundle / cores per-arch - fetch_separate_cores_and_rules() { - local outroot="$1" +stage_runtime_assets() { + local outroot="$1" + local rid="$2" - 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)" - } + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" if [[ "$FORCE_NETCORE" -eq 0 ]]; then - if download_v2rayn_bundle "$WORKDIR/$PKGROOT" "$RID_DIR"; then + if populate_assets_zip_mode "$outroot" "$rid"; then echo "[*] Using v2rayN bundle archive." else echo "[*] Bundle failed, fallback to separate core + rules." - fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT" + populate_assets_netcore_mode "$outroot" "$rid" fi else echo "[*] --netcore specified: use separate core + rules." - fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT" + populate_assets_netcore_mode "$outroot" "$rid" fi +} + +describe_target() { + local short="$1" - # Tarball - mkdir -p "$SOURCEDIR" - tar -C "$WORKDIR" -czf "$SOURCEDIR/$PKGROOT.tar.gz" "$PKGROOT" + case "$short" in + riscv64) printf '%s\n%s\n%s\n' "linux-riscv64" "riscv64" "riscv64" ;; + *) echo "Unknown arch '$short' (use riscv64)" >&2; return 1 ;; + esac +} + +publish_binary() { + local rid="$1" + + dotnet clean "$PROJECT" -c Release + rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true + dotnet restore "$PROJECT" + dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true +} - # SPEC - local SPECFILE="$SPECDIR/v2rayN.spec" - mkdir -p "$SPECDIR" - cat > "$SPECFILE" <<'SPEC' +write_spec_file() { + local specfile="$1" + + cat > "$specfile" <<'SPEC' %global debug_package %{nil} %undefine _debuginfo_subpackages %undefine _debugsource_packages -# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures) %global __requires_exclude ^liblttng-ust\.so\..*$ Name: v2rayN @@ -580,7 +508,6 @@ BugURL: https://github.com/2dust/v2rayN/issues ExclusiveArch: riscv64 Source0: __PKGROOT__.tar.gz -# Runtime dependencies (Avalonia / X11 / Fonts / GL) Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL Requires: glibc >= 2.34 Requires: fontconfig >= 2.13.1 @@ -601,32 +528,23 @@ https://github.com/2dust/v2rayN %setup -q -n __PKGROOT__ %build -# no build %install 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 || : -[ -f %{buildroot}/opt/v2rayN/libSkiaSharp.so ] && chmod 0755 %{buildroot}/opt/v2rayN/libSkiaSharp.so || : -[ -f %{buildroot}/opt/v2rayN/libHarfBuzzSharp.so ] && chmod 0755 %{buildroot}/opt/v2rayN/libHarfBuzzSharp.so || : -[ -f %{buildroot}/opt/v2rayN/libe_sqlite3.so ] && chmod 0755 %{buildroot}/opt/v2rayN/libe_sqlite3.so || : -# Launcher (prefer native ELF first, then DLL fallback) install -dm0755 %{buildroot}%{_bindir} install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF' #!/usr/bin/bash set -euo pipefail DIR="/opt/v2rayN" -export LD_LIBRARY_PATH="$DIR:${LD_LIBRARY_PATH:-}" -# Prefer native apphost if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi -# DLL fallback for dll in v2rayN.Desktop.dll v2rayN.dll; do if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi done @@ -636,7 +554,6 @@ ls -l "$DIR" >&2 || true exit 1 EOF -# Desktop file install -dm0755 %{buildroot}%{_datadir}/applications install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF' [Desktop Entry] @@ -649,7 +566,6 @@ Terminal=false Categories=Network; EOF -# Icon install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png @@ -668,36 +584,112 @@ install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons %{_datadir}/icons/hicolor/256x256/apps/v2rayn.png SPEC - # Replace placeholders - sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE" - sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE" + sed -i "s/__VERSION__/${VERSION}/g" "$specfile" + sed -i "s/__PKGROOT__/${PKGROOT}/g" "$specfile" +} + +package_binary() { + local short="$1" + local rid="$2" + local rpm_target="$3" + local archdir="$4" + local pubdir="" + local workdir="" + local specfile="" + local sourcedir="" + local specdir="" + local project_dir="" + local icon_candidate="" + local f="" + + pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish" + [[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; } + + workdir="$(mktemp -d)" + trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN + + mkdir -p "$workdir/$PKGROOT" + cp -a "$pubdir/." "$workdir/$PKGROOT/" + + 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" - # Build RPM for this arch - rpmbuild -ba "$SPECFILE" --target "$rpm_target" + stage_runtime_assets "$workdir/$PKGROOT" "$rid" + + rpmdev-setuptree + sourcedir="${RPM_TOPDIR}/SOURCES" + specdir="${RPM_TOPDIR}/SPECS" + specfile="${specdir}/v2rayN.spec" + + mkdir -p "$sourcedir" "$specdir" + tar -C "$workdir" -czf "$sourcedir/$PKGROOT.tar.gz" "$PKGROOT" + + write_spec_file "$specfile" + rpmbuild -ba "$specfile" --target "$rpm_target" echo "Build done for $short. RPM at:" - local f - for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do + for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do [[ -e "$f" ]] || continue echo " $f" BUILT_RPMS+=("$f") done } -# ===== Arch selection and build orchestration ========================================= -targets=(riscv64) +select_targets() { + printf '%s\n' riscv64 +} -for arch in "${targets[@]}"; do - build_for_arch "$arch" -done +build_one_target() { + local short="$1" + local meta=() + local rid="" + local rpm_target="" + local archdir="" + + mapfile -t meta < <(describe_target "$short") || return 1 + rid="${meta[0]}" + rpm_target="${meta[1]}" + archdir="${meta[2]}" + + echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" + publish_binary "$rid" + package_binary "$short" "$rid" "$rpm_target" "$archdir" +} + +print_summary() { + local rp="" -echo "" -echo "================ Build Summary ================" -if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then - for rp in "${BUILT_RPMS[@]}"; do - echo "$rp" + echo "" + echo "================ Build Summary ================" + if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then + for rp in "${BUILT_RPMS[@]}"; do + echo "$rp" + done + else + echo "No RPMs detected in summary (check build logs above)." + fi + echo "==============================================" +} + +main() { + local targets=() + local arch="" + + parse_args "$@" + detect_environment + install_dependencies + prepare_workspace + resolve_version + + mapfile -t targets < <(select_targets) + + for arch in "${targets[@]}"; do + build_one_target "$arch" done -else - echo "No RPMs detected in summary (check build logs above)." -fi -echo "==============================================" + + print_summary +} + +main "$@" diff --git a/package-rhel.sh b/package-rhel.sh index 310a7ab9625..2b266999684 100644 --- a/package-rhel.sh +++ b/package-rhel.sh @@ -1,116 +1,139 @@ #!/usr/bin/env bash set -euo pipefail -# Require Red Hat base branch -. /etc/os-release - -case "${ID:-}" in - rhel|rocky|almalinux|fedora|centos) - echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}" - ;; - *) - echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})." - echo "This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS." - exit 1 - ;; -esac +VERSION_ARG="" +WITH_CORE="both" +FORCE_NETCORE=0 +ARCH_OVERRIDE="" +BUILD_FROM="" +XRAY_VER="${XRAY_VER:-}" +SING_VER="${SING_VER:-}" + +MIN_KERNEL="6.12" +PKGROOT="v2rayN-publish" +PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" +RPM_TOPDIR="${HOME}/rpmbuild" + +OS_ID="" +OS_NAME="" +OS_VERSION_ID="" +HOST_ARCH="" +SCRIPT_DIR="" +PROJECT="" +VERSION="" +BUILT_ALL=0 + +declare -a BUILT_RPMS=() + +die() { + echo "$*" >&2 + exit 1 +} -# Kernel version -MIN_KERNEL="6.11" -CURRENT_KERNEL="$(uname -r)" +parse_args() { + local first_arg="${1:-}" -lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$CURRENT_KERNEL" | sort -V | head -n1)" + if [[ -n "$first_arg" && "$first_arg" != --* ]]; then + VERSION_ARG="$first_arg" + shift || true + fi -if [[ "$lowest" != "$MIN_KERNEL" ]]; then - echo "Kernel $CURRENT_KERNEL is below $MIN_KERNEL" - exit 1 -fi - -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;; + 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 ;; + *) + [[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1" + shift + ;; + esac + done + + if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then + die "You cannot specify both an explicit version and --buildfrom at the same time. + Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." + fi +} + +detect_environment() { + local current_kernel="" + local lowest="" + + . /etc/os-release + + OS_ID="${ID:-}" + OS_NAME="${NAME:-$OS_ID}" + OS_VERSION_ID="${VERSION_ID:-}" + HOST_ARCH="$(uname -m)" + + case "$OS_ID" in + rhel|rocky|almalinux|fedora|centos) + echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}" + ;; *) - if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi - shift;; + die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}). +This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS." + ;; 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 + case "$HOST_ARCH" in + x86_64|aarch64) ;; + *) die "Only supports aarch64 / x86_64" ;; + esac -# Check and install dependencies -host_arch="$(uname -m)" -[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; } + current_kernel="$(uname -r)" + lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)" + + [[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL" + echo "[OK] Kernel $current_kernel verified." +} -install_ok=0 +install_dependencies() { + local install_ok=0 -if command -v dnf >/dev/null 2>&1; then - sudo dnf -y install rpm-build rpmdevtools curl unzip tar jq rsync dotnet-sdk-8.0 \ - && install_ok=1 -fi + if command -v dnf >/dev/null 2>&1; then + sudo dnf -y install rpm-build rpmdevtools curl unzip tar jq rsync dotnet-sdk-10.0 \ + && 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, rpm, rpmdevtools, rpm-build (on Red Hat branch)" -fi + if [[ "$install_ok" -ne 1 ]]; then + echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:" + echo "dotnet-sdk 10.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on Red Hat branch)" + exit 1 + fi +} -# Root directory -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" +prepare_workspace() { + 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 + 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; } + PROJECT="$PROJECT_HINT" + [[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" + [[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found" +} choose_channel() { - # If --buildfrom provided, map it directly and skip interaction. + local ch="latest" + local sel="" + 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;; + 1) echo "latest"; return 0 ;; + 2) echo "prerelease"; return 0 ;; + 3) echo "keep"; return 0 ;; + *) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;; 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 @@ -141,29 +164,35 @@ get_latest_tag_prerelease() { | sed 's/^v//' } +sync_submodules() { + if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true + fi +} + git_try_checkout() { - # Try a series of refs and checkout when found. - local want="$1" ref="" + local want="$1" + local ref="" + if git rev-parse --git-dir >/dev/null 2>&1; then git fetch --tags --force --prune --depth=1 || true - if git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then - ref="${want}" - fi + git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want" + 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 + git checkout -f "$ref" + sync_submodules return 0 fi fi + return 1 } apply_channel_or_keep() { - local ch="$1" tag + local ch="$1" + local tag="" if [[ "$ch" == "keep" ]]; then echo "[*] Keep current repository state (no checkout)." @@ -173,102 +202,154 @@ apply_channel_or_keep() { 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; } + case "$ch" in + latest) tag="$(get_latest_tag_latest || true)" ;; + prerelease) tag="$(get_latest_tag_prerelease || true)" ;; + *) die "Failed to resolve latest tag for channel '${ch}'." ;; + esac + + [[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'." + echo "[*] Latest tag for '${ch}': ${tag}" - git_try_checkout "$tag" || { echo "Failed to checkout '${tag}'."; exit 1; } + git_try_checkout "$tag" || die "Failed to checkout '${tag}'." 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" +resolve_version() { + if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + local clean_ver="${VERSION_ARG#v}" + + if git_try_checkout "$clean_ver"; then + VERSION="$clean_ver" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + apply_channel_or_keep "$(choose_channel)" + fi else - echo "[WARN] Tag '${VERSION_ARG}' not found." - ch="$(choose_channel)" - apply_channel_or_keep "$ch" + apply_channel_or_keep "$(choose_channel)" fi else - ch="$(choose_channel)" - apply_channel_or_keep "$ch" + echo "Current directory is not a git repo; proceeding on current tree." + VERSION="${VERSION_ARG:-0.0.0}" 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}" + VERSION="${VERSION#v}" + echo "[*] GUI version resolved as: ${VERSION}" +} + +xray_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-x64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip" ;; + linux-arm64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip" ;; + *) return 1 ;; + esac +} + +singbox_url_for_rid() { + local rid="$1" + local ver="$2" + + case "$rid" in + linux-x64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz" ;; + linux-arm64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz" ;; + *) return 1 ;; + esac +} + +bundle_url_for_rid() { + local rid="$1" + + case "$rid" in + linux-x64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip" ;; + linux-arm64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip" ;; + *) return 1 ;; + esac +} -# Helpers for core download_xray() { - # Download Xray core - local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" + local outdir="$1" + local rid="$2" + local ver="${XRAY_VER:-}" + local url="" + local tmp="" + 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 + | 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 + url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; } + 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" + curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; } + unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; } + install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; } rm -rf "$tmp" } download_singbox() { - # Download sing-box - local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin cronet + local outdir="$1" + local rid="$2" + local ver="${SING_VER:-}" + local url="" + local tmp="" + local bin="" + local cronet="" + 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 + url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; } + echo "[+] Download sing-box: $url" + tmp="$(mktemp -d)" - curl -fL "$url" -o "$tmp/$tarname" - tar -C "$tmp" -xzf "$tmp/$tarname" + curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; } + 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" + + install -m 755 "$bin" "$outdir/sing-box" || { rm -rf "$tmp"; return 1; } + cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)" - [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" + [[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true + rm -rf "$tmp" } -# Move geo files to outroot/bin unify_geo_layout() { local outroot="$1" - mkdir -p "$outroot/bin" - local names=( \ - "geosite.dat" \ - "geoip.dat" \ - "geoip-only-cn-private.dat" \ - "Country.mmdb" \ - "geoip.metadb" \ + local n + local names=( + geosite.dat + geoip.dat + geoip-only-cn-private.dat + Country.mmdb + geoip.metadb ) + + mkdir -p "$outroot/bin" + for n in "${names[@]}"; do if [[ -f "$outroot/bin/xray/$n" ]]; then mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n" @@ -276,58 +357,48 @@ unify_geo_layout() { done } -# Download geo/rule assets download_geo_assets() { local outroot="$1" local bin_dir="$outroot/bin" local srss_dir="$bin_dir/srss" + local f="" + 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" + 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 + curl -fsSL -o "$bin_dir/geoip.metadb" "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" + + 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/refs/heads/rule-set-geoip/$f" 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 + + 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/refs/heads/rule-set-geosite/$f" done - # Unify to bin unify_geo_layout "$outroot" } -# Prefer the prebuilt v2rayN core bundle; then unify geo layout -download_v2rayn_bundle() { - local outroot="$1" rid="$2" +populate_assets_zip_mode() { + local outroot="$1" + local 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 + local tmp="" + local nested_dir="" + + url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; } + 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; } + + tmp="$(mktemp -d)" + curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; } + unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; return 1; } if [[ -d "$tmp/bin" ]]; then mkdir -p "$outroot/bin" @@ -339,7 +410,6 @@ download_v2rayn_bundle() { 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" @@ -347,109 +417,74 @@ download_v2rayn_bundle() { rm -rf "$nested_dir" fi - # Unify to bin/ unify_geo_layout "$outroot" + rm -rf "$tmp" echo "[+] Bundle extracted to $outroot" } -# ===== Build results collection for --arch all ======================================== -BUILT_RPMS=() # Will collect absolute paths of built RPMs -BUILT_ALL=0 # Flag to know if we should print the final summary +populate_assets_netcore_mode() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" -# ===== Build (single-arch) function ==================================================== -build_for_arch() { - # $1: target short arch: x64 | arm64 - local short="$1" - local rid rpm_target archdir - case "$short" in - x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;; - arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;; - *) echo "Unknown arch '$short' (use x64|arm64)"; return 1;; - esac + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)" + fi - echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)" + fi - # .NET publish (self-contained) for this RID - dotnet clean "$PROJECT" -c Release - rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true + download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)" +} - dotnet restore "$PROJECT" - dotnet publish "$PROJECT" \ - -c Release -r "$rid" \ - -p:PublishSingleFile=false \ - -p:SelfContained=true - - # Per-arch variables (scoped) - 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; } - - # Per-arch working area - local PKGROOT="v2rayN-publish" - local WORKDIR - WORKDIR="$(mktemp -d)" - trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN - - # rpmbuild topdir selection - local TOPDIR SPECDIR SOURCEDIR - rpmdev-setuptree - TOPDIR="${HOME}/rpmbuild" - SPECDIR="${TOPDIR}/SPECS" - SOURCEDIR="${TOPDIR}/SOURCES" - - # Stage publish content - mkdir -p "$WORKDIR/$PKGROOT" - cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/" - - # Required icon - local ICON_CANDIDATE - 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" - - # Bundle / cores per-arch - 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)" - } +stage_runtime_assets() { + local outroot="$1" + local rid="$2" + + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" if [[ "$FORCE_NETCORE" -eq 0 ]]; then - if download_v2rayn_bundle "$WORKDIR/$PKGROOT" "$RID_DIR"; then + if populate_assets_zip_mode "$outroot" "$rid"; then echo "[*] Using v2rayN bundle archive." else echo "[*] Bundle failed, fallback to separate core + rules." - fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT" + populate_assets_netcore_mode "$outroot" "$rid" fi else echo "[*] --netcore specified: use separate core + rules." - fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT" + populate_assets_netcore_mode "$outroot" "$rid" fi +} - # Tarball - mkdir -p "$SOURCEDIR" - tar -C "$WORKDIR" -czf "$SOURCEDIR/$PKGROOT.tar.gz" "$PKGROOT" +describe_target() { + local short="$1" - # SPEC - local SPECFILE="$SPECDIR/v2rayN.spec" - mkdir -p "$SPECDIR" - cat > "$SPECFILE" <<'SPEC' + case "$short" in + x64) printf '%s\n%s\n%s\n' "linux-x64" "x86_64" "x86_64" ;; + arm64) printf '%s\n%s\n%s\n' "linux-arm64" "aarch64" "aarch64" ;; + *) echo "Unknown arch '$short' (use x64|arm64)" >&2; return 1 ;; + esac +} + +publish_binary() { + local rid="$1" + + dotnet clean "$PROJECT" -c Release + rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true + dotnet restore "$PROJECT" + dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true +} + +write_spec_file() { + local specfile="$1" + + cat > "$specfile" <<'SPEC' %global debug_package %{nil} %undefine _debuginfo_subpackages %undefine _debugsource_packages -# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures) %global __requires_exclude ^liblttng-ust\.so\..*$ Name: v2rayN @@ -462,7 +497,6 @@ BugURL: https://github.com/2dust/v2rayN/issues ExclusiveArch: aarch64 x86_64 Source0: __PKGROOT__.tar.gz -# Runtime dependencies (Avalonia / X11 / Fonts / GL) Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL Requires: glibc >= 2.34 Requires: fontconfig >= 2.13.1 @@ -483,28 +517,23 @@ https://github.com/2dust/v2rayN %setup -q -n __PKGROOT__ %build -# no build %install 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} install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF' #!/usr/bin/bash set -euo pipefail DIR="/opt/v2rayN" -# Prefer native apphost if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi -# DLL fallback for dll in v2rayN.Desktop.dll v2rayN.dll; do if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi done @@ -514,7 +543,6 @@ ls -l "$DIR" >&2 || true exit 1 EOF -# Desktop file install -dm0755 %{buildroot}%{_datadir}/applications install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF' [Desktop Entry] @@ -527,7 +555,6 @@ Terminal=false Categories=Network; EOF -# Icon install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png @@ -546,45 +573,129 @@ install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons %{_datadir}/icons/hicolor/256x256/apps/v2rayn.png SPEC - # Replace placeholders - sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE" - sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE" + sed -i "s/__VERSION__/${VERSION}/g" "$specfile" + sed -i "s/__PKGROOT__/${PKGROOT}/g" "$specfile" +} - # Build RPM for this arch - rpmbuild -ba "$SPECFILE" --target "$rpm_target" +package_binary() { + local short="$1" + local rid="$2" + local rpm_target="$3" + local archdir="$4" + local pubdir="" + local workdir="" + local specfile="" + local sourcedir="" + local specdir="" + local project_dir="" + local icon_candidate="" + local f="" + + pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish" + [[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; } + + workdir="$(mktemp -d)" + trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN + + mkdir -p "$workdir/$PKGROOT" + cp -a "$pubdir/." "$workdir/$PKGROOT/" + + 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" + + stage_runtime_assets "$workdir/$PKGROOT" "$rid" + + rpmdev-setuptree + sourcedir="${RPM_TOPDIR}/SOURCES" + specdir="${RPM_TOPDIR}/SPECS" + specfile="${specdir}/v2rayN.spec" + + mkdir -p "$sourcedir" "$specdir" + tar -C "$workdir" -czf "$sourcedir/$PKGROOT.tar.gz" "$PKGROOT" + + write_spec_file "$specfile" + rpmbuild -ba "$specfile" --target "$rpm_target" echo "Build done for $short. RPM at:" - local f - for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do + for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do [[ -e "$f" ]] || continue echo " $f" BUILT_RPMS+=("$f") done } -# ===== Arch selection and build orchestration ========================================= -case "${ARCH_OVERRIDE:-}" in - 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 +select_targets() { + case "${ARCH_OVERRIDE:-}" in + all) printf '%s\n' x64 arm64 ;; + x64|amd64) printf '%s\n' x64 ;; + arm64|aarch64) printf '%s\n' arm64 ;; + "") + case "$HOST_ARCH" in + x86_64) printf '%s\n' x64 ;; + aarch64) printf '%s\n' arm64 ;; + *) return 1 ;; + esac + ;; + *) + echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all." >&2 + return 1 + ;; + esac +} -# Print Both arches information -if [[ "$BUILT_ALL" -eq 1 ]]; then - echo "" - echo "================ Build Summary (both architectures) ================" - if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then - for rp in "${BUILT_RPMS[@]}"; do - echo "$rp" - done - else - echo "No RPMs detected in summary (check build logs above)." +build_one_target() { + local short="$1" + local meta=() + local rid="" + local rpm_target="" + local archdir="" + + mapfile -t meta < <(describe_target "$short") || return 1 + rid="${meta[0]}" + rpm_target="${meta[1]}" + archdir="${meta[2]}" + + echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" + publish_binary "$rid" + package_binary "$short" "$rid" "$rpm_target" "$archdir" +} + +print_summary() { + if [[ "$BUILT_ALL" -eq 1 ]]; then + local rp="" + echo "" + echo "================ Build Summary (both architectures) ================" + if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then + for rp in "${BUILT_RPMS[@]}"; do + echo "$rp" + done + else + echo "No RPMs detected in summary (check build logs above)." + fi + echo "====================================================================" fi - echo "====================================================================" -fi +} + +main() { + local targets=() + local arch="" + + parse_args "$@" + detect_environment + install_dependencies + prepare_workspace + resolve_version + + mapfile -t targets < <(select_targets) + [[ "${ARCH_OVERRIDE:-}" == "all" ]] && BUILT_ALL=1 || BUILT_ALL=0 + + for arch in "${targets[@]}"; do + build_one_target "$arch" + done + + print_summary +} + +main "$@" \ No newline at end of file diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index cca05716391..3c7a8a3d482 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,14 +1,13 @@ - 7.21.2 + 7.22.5 - net8.0 + net10.0 true true - CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200 annotations enable 2dust diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 4cdb0ad7006..086c726f88a 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -1,36 +1,39 @@ - - true - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + true + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/GlobalHotKeys b/v2rayN/GlobalHotKeys index 50f615b671f..569a95bb0fd 160000 --- a/v2rayN/GlobalHotKeys +++ b/v2rayN/GlobalHotKeys @@ -1 +1 @@ -Subproject commit 50f615b671ff8d4a6a850aed19da5f94f58b5d96 +Subproject commit 569a95bb0fd2280d8d5581250aae54ecc2122d10 diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs index ee329845007..105310927c1 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs @@ -99,7 +99,8 @@ private static bool ContainsCycleDependencyMessage(string message) { return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase) || message.Contains("循环依赖", StringComparison.Ordinal) - || message.Contains("循環依賴", StringComparison.Ordinal); + || message.Contains("循環依賴", StringComparison.Ordinal) + || message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase); } private static async Task UpsertProfilesAsync(params ProfileItem[] profiles) diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs index 812a5a32aad..63bc45cac35 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs @@ -187,7 +187,7 @@ public static CoreConfigContext CreateContext(Config config, ProfileItem node, E SimpleDnsItem = config.SimpleDNSItem, AllProxiesMap = new Dictionary { [node.IndexId] = node }, FullConfigTemplate = null, - IsTunEnabled = false, + IsTunEnabled = config.TunModeItem.EnableTun, ProtectDomainList = [], }; } @@ -206,4 +206,12 @@ public static Config CreateConfigWithBootstrapDNS(ECoreType coreType, string boo config.SimpleDNSItem.BootstrapDNS = bootstrapDns; return config; } + + public static Config CreateConfigWithTunRouteExcludeAddress(ECoreType coreType) + { + var config = CreateConfig(coreType); + config.TunModeItem.EnableTun = true; + config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"]; + return config; + } } diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index 29b26648b3c..4847050d8c1 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -536,4 +536,27 @@ public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig() directOutbound.Should().NotBeNull(); directOutbound!.settings.domainStrategy.Should().Be("UseIPv4"); } + + [Fact] + public void GenerateClientConfigContent_TunRouteExcludeAddress() + { + var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun"); + + tunInbound.Should().NotBeNull(); + + tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0"); + tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32"); + tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31"); + } } diff --git a/v2rayN/ServiceLib.Tests/GlobalUsings.cs b/v2rayN/ServiceLib.Tests/GlobalUsings.cs new file mode 100644 index 00000000000..9d311324264 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/GlobalUsings.cs @@ -0,0 +1,40 @@ +global using System.Collections.Concurrent; +global using System.Diagnostics; +global using System.Net; +global using System.Net.NetworkInformation; +global using System.Net.Sockets; +global using System.Reactive; +global using System.Reactive.Disposables; +global using System.Reactive.Linq; +global using System.Reflection; +global using System.Runtime.InteropServices; +global using System.Security.Cryptography; +global using System.Text; +global using System.Text.Encodings.Web; +global using System.Text.Json; +global using System.Text.Json.Nodes; +global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; +global using DynamicData; +global using DynamicData.Binding; +global using ReactiveUI; +global using ReactiveUI.Fody.Helpers; +global using ServiceLib.Base; +global using ServiceLib.Common; +global using ServiceLib.Enums; +global using ServiceLib.Events; +global using ServiceLib.Handler; +global using ServiceLib.Handler.Builder; +global using ServiceLib.Handler.Fmt; +global using ServiceLib.Handler.SysProxy; +global using ServiceLib.Helper; +global using ServiceLib.Manager; +global using ServiceLib.Models.CoreConfigs; +global using ServiceLib.Models.Configs; +global using ServiceLib.Models.Dto; +global using ServiceLib.Models.Entities; +global using ServiceLib.Resx; +global using ServiceLib.Services; +global using ServiceLib.Services.CoreConfig; +global using ServiceLib.Services.Statistics; +global using SQLite; diff --git a/v2rayN/ServiceLib/Common/CountryExtension.cs b/v2rayN/ServiceLib/Common/CountryExtension.cs new file mode 100644 index 00000000000..152172d85b6 --- /dev/null +++ b/v2rayN/ServiceLib/Common/CountryExtension.cs @@ -0,0 +1,92 @@ +namespace ServiceLib.Common; + +/// +/// Extension methods for country code utilities +/// +public static class CountryExtension +{ + /// + /// Country code to emoji flag mapping for common countries + /// + private static readonly Dictionary CountryEmojiMap = new(StringComparer.OrdinalIgnoreCase) + { + // Asia + { "CN", "🇨🇳" }, // China + { "HK", "🇭🇰" }, // Hong Kong + { "TW", "🇹🇼" }, // Taiwan + { "JP", "🇯🇵" }, // Japan + { "SG", "🇸🇬" }, // Singapore + { "KR", "🇰🇷" }, // South Korea + { "TH", "🇹🇭" }, // Thailand + { "VN", "🇻🇳" }, // Vietnam + { "ID", "🇮🇩" }, // Indonesia + { "PH", "🇵🇭" }, // Philippines + { "MY", "🇲🇾" }, // Malaysia + { "IN", "🇮🇳" }, // India + { "PK", "🇵🇰" }, // Pakistan + { "BD", "🇧🇩" }, // Bangladesh + { "LK", "🇱🇰" }, // Sri Lanka + { "KH", "🇰🇭" }, // Cambodia + { "LA", "🇱🇦" }, // Laos + { "MM", "🇲🇲" }, // Myanmar + + // Americas + { "US", "🇺🇸" }, // United States + { "CA", "🇨🇦" }, // Canada + { "MX", "🇲🇽" }, // Mexico + { "BR", "🇧🇷" }, // Brazil + { "AR", "🇦🇷" }, // Argentina + { "CL", "🇨🇱" }, // Chile + { "CO", "🇨🇴" }, // Colombia + + // Europe + { "GB", "🇬🇧" }, // United Kingdom + { "DE", "🇩🇪" }, // Germany + { "FR", "🇫🇷" }, // France + { "IT", "🇮🇹" }, // Italy + { "ES", "🇪🇸" }, // Spain + { "RU", "🇷🇺" }, // Russia + { "NL", "🇳🇱" }, // Netherlands + { "CH", "🇨🇭" }, // Switzerland + { "SE", "🇸🇪" }, // Sweden + { "NO", "🇳🇴" }, // Norway + { "DK", "🇩🇰" }, // Denmark + { "FI", "🇫🇮" }, // Finland + { "PL", "🇵🇱" }, // Poland + { "CZ", "🇨🇿" }, // Czech Republic + { "AT", "🇦🇹" }, // Austria + { "GR", "🇬🇷" }, // Greece + { "PT", "🇵🇹" }, // Portugal + { "TR", "🇹🇷" }, // Turkey + { "UA", "🇺🇦" }, // Ukraine + { "RO", "🇷🇴" }, // Romania + + // Middle East & Central Asia + { "AE", "🇦🇪" }, // United Arab Emirates + { "SA", "🇸🇦" }, // Saudi Arabia + { "IL", "🇮🇱" }, // Israel + { "KZ", "🇰🇿" }, // Kazakhstan + + // Oceania + { "AU", "🇦🇺" }, // Australia + { "NZ", "🇳🇿" }, // New Zealand + + // Africa + { "ZA", "🇿🇦" }, // South Africa + { "EG", "🇪🇬" }, // Egypt + }; + + /// + /// Converts country code to flag emoji using predefined mapping + /// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳" + /// + public static string? CountryToEmoji(this string? countryCode) + { + if (countryCode.IsNullOrEmpty()) + { + return null; + } + + return CountryEmojiMap.TryGetValue(countryCode, out var emoji) ? emoji : null; + } +} diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index c0d8816d8be..9c2bd4af43c 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -864,15 +864,25 @@ private static Dictionary GetSystemHosts(string hostFile) public static Dictionary GetSystemHosts() { - var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts"); - var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics"); + if (IsWindows()) + { + var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts"); + var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics"); + + foreach (var (key, value) in hostsIcs) + { + hosts[key] = value; + } + + return hosts; + } - foreach (var (key, value) in hostsIcs) + if (IsLinux() || IsMacOS()) { - hosts[key] = value; + return GetSystemHosts("/etc/hosts"); } - return hosts; + return new Dictionary(); } public static async Task GetCliWrapOutput(string filePath, string? arg) @@ -1114,12 +1124,16 @@ public static string GetBinConfigPath(string filename = "") #region Platform + [SupportedOSPlatformGuard("windows")] public static bool IsWindows() => OperatingSystem.IsWindows(); + [SupportedOSPlatformGuard("linux")] public static bool IsLinux() => OperatingSystem.IsLinux(); + [SupportedOSPlatformGuard("macos")] public static bool IsMacOS() => OperatingSystem.IsMacOS(); + [UnsupportedOSPlatformGuard("windows")] public static bool IsNonWindows() => !OperatingSystem.IsWindows(); public static string GetExeName(string name) @@ -1214,6 +1228,16 @@ public static bool IsPackagedInstall() } public static bool SetUnixFileMode(string? fileName) + { + if (IsWindows()) + { + return false; + } + return SetUnixFileModeInternal(fileName); + } + + [UnsupportedOSPlatform("windows")] + private static bool SetUnixFileModeInternal(string? fileName) { try { diff --git a/v2rayN/ServiceLib/Common/WindowsUtils.cs b/v2rayN/ServiceLib/Common/WindowsUtils.cs index 6cd47f0a809..5792dcecfaf 100644 --- a/v2rayN/ServiceLib/Common/WindowsUtils.cs +++ b/v2rayN/ServiceLib/Common/WindowsUtils.cs @@ -2,6 +2,7 @@ namespace ServiceLib.Common; +[SupportedOSPlatform("windows")] internal static class WindowsUtils { private static readonly string _tag = "WindowsUtils"; diff --git a/v2rayN/ServiceLib/Enums/EServerColName.cs b/v2rayN/ServiceLib/Enums/EServerColName.cs index 9f50f4df270..8800cdf6071 100644 --- a/v2rayN/ServiceLib/Enums/EServerColName.cs +++ b/v2rayN/ServiceLib/Enums/EServerColName.cs @@ -12,6 +12,7 @@ public enum EServerColName SubRemarks, DelayVal, SpeedVal, + IpInfo, TodayDown, TodayUp, diff --git a/v2rayN/ServiceLib/Events/AppEvents.cs b/v2rayN/ServiceLib/Events/AppEvents.cs index 5824bfc0099..2f97bc6b27f 100644 --- a/v2rayN/ServiceLib/Events/AppEvents.cs +++ b/v2rayN/ServiceLib/Events/AppEvents.cs @@ -7,6 +7,7 @@ public static class AppEvents public static readonly EventChannel AddServerViaScanRequested = new(); public static readonly EventChannel AddServerViaClipboardRequested = new(); public static readonly EventChannel SubscriptionsUpdateRequested = new(); + public static readonly EventChannel HasUpdateNotified = new(); public static readonly EventChannel ProfilesRefreshRequested = new(); public static readonly EventChannel SubscriptionsRefreshRequested = new(); diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index ca167fa3141..cadd0c53bd5 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -55,7 +55,7 @@ public class Global public const string DnsOutboundTag = "dns"; public const string DnsTag = "dns-module"; public const string DirectDnsTag = "direct-dns"; - public const string BalancerTagSuffix = "-round"; + public const string BalancerTagSuffix = "-balancer"; public const string StreamSecurity = "tls"; public const string StreamSecurityReality = "reality"; public const string Loopback = "127.0.0.1"; @@ -149,6 +149,9 @@ public class Global public static readonly List SpeedTestUrls = [ @"https://cachefly.cachefly.net/50mb.test", + @"https://cachefly.cachefly.net/100mb.test", + @"https://cachefly.cachefly.net/1mb.test", + @"https://cachefly.cachefly.net/10mb.test", @"https://speed.cloudflare.com/__down?bytes=10000000", @"https://speed.cloudflare.com/__down?bytes=50000000", @"https://speed.cloudflare.com/__down?bytes=99999999", @@ -157,6 +160,8 @@ public class Global public static readonly List SpeedPingTestUrls = [ @"https://www.google.com/generate_204", + @"https://www.youtube.com/generate_204", + @"https://www.googlevideo.com/generate_204", @"https://www.gstatic.com/generate_204", @"https://www.apple.com/library/test/success.html", @"http://www.msftconnecttest.com/connecttest.txt" @@ -207,6 +212,10 @@ public class Global public const string NaiveQuicProtocolShare = "naive+quic://"; + public const string SOCKS5Protocol = "socks5://"; + + public const string SOCKS4Protocol = "socks4://"; + public static readonly Dictionary ProtocolShares = new() { { EConfigType.VMess, "vmess://" }, @@ -505,6 +514,7 @@ public class Global public static readonly List InboundTags = [ + "tun", "socks", "socks2", "socks3" @@ -675,15 +685,15 @@ public class Global { "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] }, { "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] }, { "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] }, - { "dns.cloudflare.com", ["104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5"] }, + { "dns.cloudflare.com", ["162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8"] }, { "dot.pub", ["1.12.12.12", "120.53.53.53"] }, { "doh.pub", ["1.12.12.12", "120.53.53.53"] }, { "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] }, { "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] }, - { "dns.sb", ["185.222.222.222", "2a09::"] }, + { "dns.sb", ["45.11.45.11", "185.222.222.222", "2a09::", "2a11::"] }, { "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] }, { "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] }, - { "engage.cloudflareclient.com", ["162.159.192.1"] } + { "engage.cloudflareclient.com", ["162.159.192.1", "2606:4700:d0::a29f:c001"] } }; public static readonly List ExpectedIPs = diff --git a/v2rayN/ServiceLib/GlobalUsings.cs b/v2rayN/ServiceLib/GlobalUsings.cs index 4553cbc4141..6e051d80410 100644 --- a/v2rayN/ServiceLib/GlobalUsings.cs +++ b/v2rayN/ServiceLib/GlobalUsings.cs @@ -8,6 +8,7 @@ global using System.Reactive.Linq; global using System.Reflection; global using System.Runtime.InteropServices; +global using System.Runtime.Versioning; global using System.Security.Cryptography; global using System.Text; global using System.Text.Encodings.Web; @@ -29,7 +30,10 @@ global using ServiceLib.Handler.SysProxy; global using ServiceLib.Helper; global using ServiceLib.Manager; -global using ServiceLib.Models; +global using ServiceLib.Models.CoreConfigs; +global using ServiceLib.Models.Configs; +global using ServiceLib.Models.Dto; +global using ServiceLib.Models.Entities; global using ServiceLib.Resx; global using ServiceLib.Services; global using ServiceLib.Services.CoreConfig; diff --git a/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs b/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs index bc86cd7f90c..acd9a5156df 100644 --- a/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs +++ b/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs @@ -41,6 +41,7 @@ public static async Task UpdateTask(Config config) #region Windows + [SupportedOSPlatform("windows")] private static async Task ClearTaskWindows() { var autoRunName = GetAutoRunNameWindows(); @@ -53,6 +54,7 @@ private static async Task ClearTaskWindows() await Task.CompletedTask; } + [SupportedOSPlatform("windows")] private static async Task SetTaskWindows() { try @@ -82,6 +84,7 @@ private static async Task SetTaskWindows() /// /// /// + [SupportedOSPlatform("windows")] public static void AutoStartTaskService(string taskName, string fileName, string description) { if (taskName.IsNullOrEmpty()) @@ -108,7 +111,8 @@ public static void AutoStartTaskService(string taskName, string fileName, string task.Settings.RunOnlyIfIdle = false; task.Settings.IdleSettings.StopOnIdleEnd = false; task.Settings.ExecutionTimeLimit = TimeSpan.Zero; - task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser, Delay = TimeSpan.FromSeconds(30) }); + task.Settings.Priority = ProcessPriorityClass.Normal; + task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser }); task.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest; task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName))); @@ -124,6 +128,7 @@ private static string GetAutoRunNameWindows() #region Linux + [SupportedOSPlatform("linux")] private static async Task ClearTaskLinux() { try @@ -137,6 +142,7 @@ private static async Task ClearTaskLinux() await Task.CompletedTask; } + [SupportedOSPlatform("linux")] private static async Task SetTaskLinux() { try @@ -157,6 +163,7 @@ private static async Task SetTaskLinux() } } + [SupportedOSPlatform("linux")] private static string GetHomePathLinux() { var homePath = Path.Combine(Utils.GetHomePath(), ".config", "autostart", $"{Global.AppName}.desktop"); @@ -168,6 +175,7 @@ private static string GetHomePathLinux() #region macOS + [SupportedOSPlatform("macos")] private static async Task ClearTaskOSX() { try @@ -187,6 +195,7 @@ private static async Task ClearTaskOSX() } } + [SupportedOSPlatform("macos")] private static async Task SetTaskOSX() { try @@ -204,6 +213,7 @@ private static async Task SetTaskOSX() } } + [SupportedOSPlatform("macos")] private static string GetLaunchAgentPathMacOS() { var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); @@ -212,6 +222,7 @@ private static string GetLaunchAgentPathMacOS() return launchAgentPath; } + [SupportedOSPlatform("macos")] private static string GenerateLaunchAgentPlist() { var exePath = Utils.GetExePath(); diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index eb1af21b136..b7481e1aaa8 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -91,6 +91,20 @@ public static async Task Build(Config config, Pr context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; } } + if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 }) + { + foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress) + { + try + { + IPNetwork2.Parse(addr); + } + catch + { + validatorResult.Errors.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr)); + } + } + } return new CoreConfigContextBuilderResult(context, validatorResult); } diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs index e94635b7041..2b8199400a6 100644 --- a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -134,6 +134,14 @@ private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreT { v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate")); } + + // Check for deprecated allowInsecure property when TLS is enabled + if (item.AllowInsecure == "true" + && item.Cert.IsNullOrEmpty() + && item.CertSha.IsNullOrEmpty()) + { + v.Warning(ResUI.MsgAllowInsecureDeprecated); + } } if (item.StreamSecurity == Global.StreamSecurityReality) diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 54a0108aa3d..22fe0ab2595 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -255,6 +255,7 @@ public static async Task AddServer(Config config, ProfileItem profileItem) item.Cert = profileItem.Cert; item.CertSha = profileItem.CertSha; item.EchConfigList = profileItem.EchConfigList; + item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName; item.Finalmask = profileItem.Finalmask; item.ProtoExtra = profileItem.ProtoExtra; item.TransportExtra = profileItem.TransportExtra; @@ -923,6 +924,7 @@ from t33 in t3b.DefaultIfEmpty() Delay = t33?.Delay ?? 0, Speed = t33?.Speed ?? 0, Sort = t33?.Sort ?? 0, + IpInfo = t33?.IpInfo ?? string.Empty, TodayDown = (t22?.TodayDown ?? 0).ToString("D16"), TodayUp = (t22?.TodayUp ?? 0).ToString("D16"), TotalDown = (t22?.TotalDown ?? 0).ToString("D16"), @@ -943,6 +945,7 @@ from t33 in t3b.DefaultIfEmpty() EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(), EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(), EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(), + EServerColName.IpInfo => lstProfile.OrderBy(t => t.IpInfo).ToList(), EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(), EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(), EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(), @@ -963,6 +966,7 @@ from t33 in t3b.DefaultIfEmpty() EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(), EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(), EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(), + EServerColName.IpInfo => lstProfile.OrderByDescending(t => t.IpInfo).ToList(), EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(), EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(), EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(), @@ -1816,9 +1820,9 @@ public static async Task AddBatchServers(Config config, string strData, str ProfileItem? activeProfile = null; if (isSub && subid.IsNotEmpty()) { - await RemoveServersViaSubid(config, subid, true); lstOriSub = await AppManager.Instance.ProfileItems(subid); activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId); + await RemoveServersViaSubid(config, subid, true); } var counter = 0; @@ -1847,7 +1851,19 @@ public static async Task AddBatchServers(Config config, string strData, str } //May be standard uri mixed with internal uri - var innerUriCount = await AddBatchServers4InnerUri(config, strData, subid, isSub); + var innerUriCount = 0; + if (Utils.IsBase64String(strData)) + { + innerUriCount = await AddBatchServers4InnerUri(config, Utils.Base64Decode(strData), subid, isSub); + } + if (innerUriCount < 1) + { + innerUriCount = await AddBatchServers4InnerUri(config, strData, subid, isSub); + } + if (innerUriCount < 1) + { + innerUriCount = await AddBatchServers4InnerUri(config, Utils.Base64Decode(strData), subid, isSub); + } if (innerUriCount > 0) { if (counter > 0) diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index 05825d934d2..6e8df9f85fb 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -4,53 +4,41 @@ public static class ConnectionHandler { private static readonly string _tag = "ConnectionHandler"; + /// + /// Runs ping and IP checks and returns a formatted result string. + /// public static async Task RunAvailabilityCheck() { var time = await GetRealPingTimeInfo(); - var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None; + var ip = time > 0 ? await GetIPInfo() : Global.None; return string.Format(ResUI.TestMeOutput, time, ip); } + /// + /// Gets IP information using the default local proxy. + /// private static async Task GetIPInfo() { - var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl; - if (url.IsNullOrEmpty()) - { - return null; - } - - var downloadHandle = new DownloadService(); - var result = await downloadHandle.TryDownloadString(url, true, ""); - if (result == null) - { - return null; - } - - var ipInfo = JsonUtils.Deserialize(result); - if (ipInfo == null) - { - return null; - } - - var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query; - var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code; + var webProxy = await GetWebProxy(); - return $"({country ?? "unknown"}) {ip}"; + var ipInfo = await GetIPInfo(webProxy); + return ipInfo?.ToString() ?? Global.None; } + /// + /// Measures real ping time using configured test URL. + /// private static async Task GetRealPingTimeInfo() { var responseTime = -1; try { - var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); - var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}"); - var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; + var webProxy = await GetWebProxy(); for (var i = 0; i < 2; i++) { - responseTime = await GetRealPingTime(url, webProxy, 10); + responseTime = await GetRealPingTime(webProxy, 10); if (responseTime > 0) { break; @@ -66,8 +54,21 @@ private static async Task GetRealPingTimeInfo() return responseTime; } - public static async Task GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) + /// + /// Creates local SOCKS proxy instance. + /// + private static async Task GetWebProxy() { + var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); + return new WebProxy($"socks5://{Global.Loopback}:{port}"); + } + + /// + /// Measures response time by sending HTTP requests through proxy. + /// + public static async Task GetRealPingTime(IWebProxy? webProxy, int downloadTimeout) + { + var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; var responseTime = -1; try { @@ -95,4 +96,41 @@ public static async Task GetRealPingTime(string url, IWebProxy? webProxy, i } return responseTime; } + + /// + /// Gets IP and country information through specified proxy. + /// + public static async Task GetIPInfo(IWebProxy? webProxy) + { + try + { + var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl; + if (url.IsNullOrEmpty()) + { + return null; + } + + var downloadHandle = new DownloadService(); + var result = await downloadHandle.TryDownloadString(url, webProxy, ""); + if (result == null) + { + return null; + } + + var ipInfo = JsonUtils.Deserialize(result); + if (ipInfo == null) + { + return null; + } + + var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query; + var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? "unknown"; + + return new IpInfoResult(country, ip); + } + catch + { + return null; + } + } } diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index ff7d6d20ea2..0f82242763e 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -73,6 +73,10 @@ protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dicti { dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList)); } + if (item.VerifyPeerCertByName.IsNotEmpty()) + { + dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName)); + } if (item.CertSha.IsNotEmpty()) { dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha)); @@ -227,6 +231,7 @@ protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item.SpiderX = GetQueryDecoded(query, "spx"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); item.EchConfigList = GetQueryDecoded(query, "ech"); + item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn"); item.CertSha = GetQueryDecoded(query, "pcs"); var finalmaskDecoded = GetQueryDecoded(query, "fm"); diff --git a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs index 611e515916d..7691d874ba2 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs @@ -53,7 +53,9 @@ public class FmtHandler { return ShadowsocksFmt.Resolve(str, out msg); } - else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS])) + else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS]) + || str.StartsWith(Global.SOCKS5Protocol) + || str.StartsWith(Global.SOCKS4Protocol)) { return SocksFmt.Resolve(str, out msg); } @@ -65,7 +67,8 @@ public class FmtHandler { return VLESSFmt.Resolve(str, out msg); } - else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2]) || str.StartsWith(Global.Hysteria2ProtocolShare)) + else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2]) + || str.StartsWith(Global.Hysteria2ProtocolShare)) { return Hysteria2Fmt.Resolve(str, out msg); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs index 16420e74c39..c9ad5844d46 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs @@ -230,6 +230,9 @@ public class InnerFmt jsonObj["TransportExtraObj"] = transportExtraObj; jsonObj.Remove("TransportExtra"); } + // remove subid and isSub + jsonObj.Remove("Subid"); + jsonObj.Remove("IsSub"); // Remove empty properties to reduce the length of the exported string RemoveEmptyJson(jsonObj); var jsonStr = JsonUtils.Serialize(jsonObj, false); diff --git a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs index b3a543010b7..62b36e07382 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs @@ -99,12 +99,17 @@ public class SocksFmt : BaseFmt }; // parse base64 UserInfo var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo); - var userInfo = Utils.Base64Decode(rawUserInfo); - var userInfoParts = userInfo.Split([':'], 2); - if (userInfoParts.Length == 2) + if (rawUserInfo.IsNotEmpty()) { - item.Username = userInfoParts.First(); - item.Password = userInfoParts[1]; + var userInfoParts = rawUserInfo.Contains(':') + ? rawUserInfo.Split(":", 2) + : Utils.Base64Decode(rawUserInfo).Split(":", 2); + + if (userInfoParts.Length == 2) + { + item.Username = userInfoParts.First(); + item.Password = userInfoParts.Last(); + } } return item; diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs index 4929c72ee33..14d61d9ca19 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs @@ -1,5 +1,6 @@ namespace ServiceLib.Handler.SysProxy; +[SupportedOSPlatform("linux")] public static class ProxySettingLinux { private static readonly string _proxySetFileName = $"{Global.ProxySetLinuxShellFileName.Replace(Global.NamespaceSample, "")}.sh"; diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs index 56fbe24d558..b598a06cd9e 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs @@ -1,5 +1,6 @@ namespace ServiceLib.Handler.SysProxy; +[SupportedOSPlatform("macos")] public static class ProxySettingOSX { private static readonly string _proxySetFileName = $"{Global.ProxySetOSXShellFileName.Replace(Global.NamespaceSample, "")}.sh"; diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs index 8dc2f335adc..614437d7cb9 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs @@ -2,6 +2,7 @@ namespace ServiceLib.Handler.SysProxy; +[SupportedOSPlatform("windows")] public static class ProxySettingWindows { private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings"; diff --git a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs index 21453591e7b..c525da29be6 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs @@ -88,6 +88,7 @@ private static void GetWindowsProxyString(Config config, int port, out string st } } + [SupportedOSPlatform("windows")] private static async Task SetWindowsProxyPac(int port) { var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac); diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 34f6b3f1ba8..b532ea1667d 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -48,6 +48,13 @@ public bool IsRunningCore(ECoreType type) } } + public Dictionary LastCheckUpdateResults { get; set; } = new(); + + public void SetLastCheckUpdateResult(ECoreType coreType, string result) + { + LastCheckUpdateResults[coreType] = result; + } + #endregion Property #region App diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs index 0a58e2deccf..7f5d388984d 100644 --- a/v2rayN/ServiceLib/Manager/CertPemManager.cs +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -4,16 +4,15 @@ namespace ServiceLib.Manager; /// -/// Manager for certificate operations with CA pinning to prevent MITM attacks +/// Manager for certificate operations with CA pinning to prevent MITM attacks /// public class CertPemManager { private static readonly string _tag = "CertPemManager"; private static readonly Lazy _instance = new(() => new()); - public static CertPemManager Instance => _instance.Value; /// - /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks + /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks /// private static readonly HashSet TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase) { @@ -24,21 +23,17 @@ public class CertPemManager "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root "85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2 "18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3 - "CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root "C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA "1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA "3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA "4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA "7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA "62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2 - "F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA - "4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA "0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority "1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority "41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA "E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna "C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority - "EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA "6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány "3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009 "CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3 @@ -46,10 +41,6 @@ public class CertPemManager "45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2 "2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2 "568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2 - "0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial - "0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking - "70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium - "BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC "5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA "BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority "513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2 @@ -62,7 +53,6 @@ public class CertPemManager "E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2 "9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1 "59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA - "DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1 "91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2 "F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011 "8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3 @@ -116,16 +106,12 @@ public class CertPemManager "C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017 "BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017 "657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2 - "97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority - "945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority - "55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority "88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority "554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS "319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45 "5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45 "4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46 "CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46 - "9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020 "FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA "6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA "FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA @@ -179,7 +165,6 @@ public class CertPemManager "578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020 "78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023 "EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023 - "BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB "3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA "3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2 "3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12 @@ -200,10 +185,13 @@ public class CertPemManager "B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023 }; + public static CertPemManager Instance => _instance.Value; + /// - /// Get certificate in PEM format from a server with CA pinning validation + /// Get certificate in PEM format from a server with CA pinning validation /// - public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false) + public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, + List? verifyPeerCertByName = null, bool allowInsecure = false) { try { @@ -216,13 +204,14 @@ public class CertPemManager await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => - ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure)); + ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], + allowInsecure)); await using var ssl = new SslStream(client.GetStream(), false, callback); var sslOptions = new SslClientAuthenticationOptions { TargetHost = serverName, - RemoteCertificateValidationCallback = callback + RemoteCertificateValidationCallback = callback, }; await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); @@ -249,9 +238,10 @@ public class CertPemManager } /// - /// Get certificate chain in PEM format from a server with CA pinning validation + /// Get certificate chain in PEM format from a server with CA pinning validation /// - public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false) + public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, + List? verifyPeerCertByName = null, bool allowInsecure = false) { var pemList = new List(); try @@ -265,13 +255,14 @@ public class CertPemManager await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => - ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure)); + ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], + allowInsecure)); await using var ssl = new SslStream(client.GetStream(), false, callback); var sslOptions = new SslClientAuthenticationOptions { TargetHost = serverName, - RemoteCertificateValidationCallback = callback + RemoteCertificateValidationCallback = callback, }; await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); @@ -301,13 +292,14 @@ public class CertPemManager } /// - /// Validate server certificate with CA pinning + /// Validate server certificate with CA pinning /// - private bool ValidateServerCertificate( + private static bool ValidateServerCertificate( object _, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors, + List verifyPeerCertByName, bool allowInsecure) { if (certificate == null) @@ -321,22 +313,21 @@ private bool ValidateServerCertificate( return true; } - // Check certificate name mismatch - if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) - { - return false; - } - // Build certificate chain var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); var certChain = chain ?? new X509Chain(); - certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; - certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; - certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; - certChain.ChainPolicy.VerificationTime = DateTime.Now; + if (chain == null) + { + certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + certChain.ChainPolicy.VerificationTime = DateTime.UtcNow; - certChain.Build(cert2); + if (!certChain.Build(cert2)) + { + return false; + } + } // Find root CA if (certChain.ChainElements.Count == 0) @@ -344,10 +335,37 @@ private bool ValidateServerCertificate( return false; } - var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate; + var rootCert = certChain.ChainElements[^1].Certificate; var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256); - return TrustedCaThumbprints.Contains(rootThumbprint); + if (!TrustedCaThumbprints.Contains(rootThumbprint)) + { + return false; + } + + if (!sslPolicyErrors.HasFlag( + SslPolicyErrors.RemoteCertificateNameMismatch)) + { + return true; + } + + if (verifyPeerCertByName.Count == 0) + { + return false; + } + + foreach (var ext in cert2.Extensions) + { + if (ext is not X509SubjectAlternativeNameExtension san) + { + continue; + } + + return san.EnumerateDnsNames().Any(dnsName => + verifyPeerCertByName.Contains(dnsName, StringComparer.OrdinalIgnoreCase)); + } + + return false; } public static string ExportCertToPem(X509Certificate2 cert) @@ -358,8 +376,8 @@ public static string ExportCertToPem(X509Certificate2 cert) } /// - /// Parse concatenated PEM certificates string into a list of individual certificates - /// Normalizes format: removes line breaks from base64 content for better compatibility + /// Parse concatenated PEM certificates string into a list of individual certificates + /// Normalizes format: removes line breaks from base64 content for better compatibility /// /// Concatenated PEM certificates string (supports both \r\n and \n line endings) /// List of individual PEM certificate strings with normalized format @@ -411,18 +429,13 @@ public static List ParsePemChain(string pemChain) } /// - /// Concatenate a list of PEM certificates into a single string + /// Concatenate a list of PEM certificates into a single string /// /// List of individual PEM certificate strings /// Concatenated PEM certificates string - public static string ConcatenatePemChain(IEnumerable pemList) + public static string ConcatenatePemChain(IEnumerable? pemList) { - if (pemList == null) - { - return string.Empty; - } - - return string.Concat(pemList); + return pemList == null ? string.Empty : string.Concat(pemList); } public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false) @@ -431,11 +444,7 @@ public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = { var cert = X509Certificate2.CreateFromPem(pemCert); var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256); - if (includeColon) - { - return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))); - } - return thumbprint; + return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint; } catch { diff --git a/v2rayN/ServiceLib/Manager/ClashApiManager.cs b/v2rayN/ServiceLib/Manager/ClashApiManager.cs index e34f838dacd..313060dbeb4 100644 --- a/v2rayN/ServiceLib/Manager/ClashApiManager.cs +++ b/v2rayN/ServiceLib/Manager/ClashApiManager.cs @@ -1,4 +1,4 @@ -using static ServiceLib.Models.ClashProxies; +using static ServiceLib.Models.Dto.ClashProxies; namespace ServiceLib.Manager; diff --git a/v2rayN/ServiceLib/Manager/CoreInfoManager.cs b/v2rayN/ServiceLib/Manager/CoreInfoManager.cs index e4b5c7f30b0..bda3108ae44 100644 --- a/v2rayN/ServiceLib/Manager/CoreInfoManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreInfoManager.cs @@ -50,6 +50,50 @@ public string GetCoreExecFile(CoreInfo? coreInfo, out string msg) return fileName; } + public List GetCheckUpdateCoreTypes() + { + var lst = new List(); + + if (RuntimeInformation.ProcessArchitecture != Architecture.X86) + { + if (IsCheckUpdateSupported(ECoreType.v2rayN)) + { + lst.Add(ECoreType.v2rayN); + } + + if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)) + { + lst.Add(ECoreType.Xray); + lst.Add(ECoreType.mihomo); + lst.Add(ECoreType.sing_box); + } + } + + return lst; + } + + public bool IsCheckUpdateSupported(ECoreType type) + { + return type switch + { + ECoreType.v2rayN => !Utils.IsPackagedInstall(), + ECoreType.Xray => true, + ECoreType.mihomo => true, + ECoreType.sing_box => true, + _ => false, + }; + } + + public bool GetCheckPreRelease(ECoreType type, bool preRelease) + { + return type switch + { + ECoreType.v2rayN => preRelease, + ECoreType.Xray => preRelease, + _ => false, + }; + } + private void InitCoreInfo() { var urlN = GetCoreUrl(ECoreType.v2rayN); @@ -69,6 +113,7 @@ private void InitCoreInfo() DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip", DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip", DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.zip", + DownloadUrlLinuxLoong64 = urlN + "/download/{0}/v2rayN-linux-loong64.zip", DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip", DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip", }, @@ -113,6 +158,7 @@ private void InitCoreInfo() DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip", DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip", DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.zip", + DownloadUrlLinuxLoong64 = urlXray + "/download/{0}/Xray-linux-loong64.zip", DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip", DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip", Match = "Xray", @@ -136,6 +182,7 @@ private void InitCoreInfo() DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz", DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz", DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{0}.gz", + DownloadUrlLinuxLoong64 = urlMihomo + "/download/{0}/mihomo-linux-loong64-abi2-{0}.gz", DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz", DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz", Match = "Mihomo", @@ -179,6 +226,7 @@ private void InitCoreInfo() DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz", DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz", DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.tar.gz", + DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz", DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz", DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz", Match = "sing-box", @@ -270,6 +318,7 @@ private static string GetCoreUrl(ECoreType eCoreType) names.Add("mihomo-linux-amd64"); names.Add("mihomo-linux-arm64"); names.Add("mihomo-linux-riscv64"); + names.Add("mihomo-linux-loong64-abi2"); } else if (Utils.IsMacOS()) { diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index c309680a036..41d2343b25c 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -8,6 +8,7 @@ public class CoreManager private static readonly Lazy _instance = new(() => new()); public static CoreManager Instance => _instance.Value; private Config _config; + [SupportedOSPlatform("windows")] private WindowsJobService? _processJob; private ProcessService? _processService; private ProcessService? _processPreService; diff --git a/v2rayN/ServiceLib/Manager/ProfileExManager.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs index 739bd550a7b..cbe90a09d61 100644 --- a/v2rayN/ServiceLib/Manager/ProfileExManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -150,6 +150,14 @@ public void SetTestMessage(string indexId, string message) IndexIdEnqueue(indexId); } + public void SetTestIpInfo(string indexId, string ipInfo) + { + var profileEx = GetProfileExItem(indexId); + + profileEx.IpInfo = ipInfo; + IndexIdEnqueue(indexId); + } + public void SetSort(string indexId, int sort) { var profileEx = GetProfileExItem(indexId); diff --git a/v2rayN/ServiceLib/Manager/TaskManager.cs b/v2rayN/ServiceLib/Manager/TaskManager.cs index d1c7315785f..1e5b0c706da 100644 --- a/v2rayN/ServiceLib/Manager/TaskManager.cs +++ b/v2rayN/ServiceLib/Manager/TaskManager.cs @@ -70,6 +70,18 @@ private async Task ScheduledTasks() } } + //Execute once 24 hour + if (numOfExecuted % 1440 == 1) + { + try + { + await UpdateTaskRunCheckUpdate(); + } + catch (Exception ex) + { + Logging.SaveLog("ScheduledTasks - UpdateTaskRunCheckUpdate", ex); + } + } numOfExecuted++; } } @@ -117,4 +129,23 @@ private async Task UpdateTaskRunGeo(int hours) }).UpdateGeoFileAll(); } } + + private async Task UpdateTaskRunCheckUpdate() + { + Logging.SaveLog("Execute check update"); + + var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask); + + var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.CheckPreReleaseUpdate); + foreach (var msg in msgs) + { + await _updateFunc?.Invoke(false, msg); + } + NoticeManager.Instance.Enqueue(string.Join("\n", msgs)); + + if (msgs.Count > 0) + { + AppEvents.HasUpdateNotified.Publish(true); + } + } } diff --git a/v2rayN/ServiceLib/Models/Config.cs b/v2rayN/ServiceLib/Models/Configs/Config.cs similarity index 97% rename from v2rayN/ServiceLib/Models/Config.cs rename to v2rayN/ServiceLib/Models/Configs/Config.cs index 738ee286202..69770d3cb1d 100644 --- a/v2rayN/ServiceLib/Models/Config.cs +++ b/v2rayN/ServiceLib/Models/Configs/Config.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Configs; [Serializable] public class Config diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs similarity index 97% rename from v2rayN/ServiceLib/Models/ConfigItems.cs rename to v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index b23a8fc3594..e3026184ad0 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Configs; [Serializable] public class CoreBasicItem @@ -148,6 +148,7 @@ public class TunModeItem public bool EnableIPv6Address { get; set; } public string IcmpRouting { get; set; } public bool EnableLegacyProtect { get; set; } + public List? RouteExcludeAddress { get; set; } } [Serializable] @@ -159,6 +160,8 @@ public class SpeedTestItem public int MixedConcurrencyCount { get; set; } public string IPAPIUrl { get; set; } public string UdpTestTarget { get; set; } + public int? SpeedTestPageSize { get; set; } + public int? SpeedTestDelayInterval { get; set; } } [Serializable] diff --git a/v2rayN/ServiceLib/Models/CoreConfigContext.cs b/v2rayN/ServiceLib/Models/CoreConfigs/CoreConfigContext.cs similarity index 95% rename from v2rayN/ServiceLib/Models/CoreConfigContext.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/CoreConfigContext.cs index 4b64efca867..e1100868d86 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigContext.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/CoreConfigContext.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.CoreConfigs; public record CoreConfigContext { diff --git a/v2rayN/ServiceLib/Models/CoreInfo.cs b/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs similarity index 89% rename from v2rayN/ServiceLib/Models/CoreInfo.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs index 087f57fe041..1cb23af03da 100644 --- a/v2rayN/ServiceLib/Models/CoreInfo.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.CoreConfigs; [Serializable] public class CoreInfo @@ -13,6 +13,7 @@ public class CoreInfo public string? DownloadUrlLinux64 { get; set; } public string? DownloadUrlLinuxArm64 { get; set; } public string? DownloadUrlLinuxRiscV64 { get; set; } + public string? DownloadUrlLinuxLoong64 { get; set; } public string? DownloadUrlOSX64 { get; set; } public string? DownloadUrlOSXArm64 { get; set; } public string? Match { get; set; } diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs similarity index 99% rename from v2rayN/ServiceLib/Models/SingboxConfig.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs index 0e2bb530dcc..a5249acb778 100644 --- a/v2rayN/ServiceLib/Models/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.CoreConfigs; public class SingboxConfig { @@ -110,6 +110,7 @@ public class Inbound4Sbox public bool? endpoint_independent_nat { get; set; } public string? stack { get; set; } public List users { get; set; } + public List? route_exclude_address { get; set; } } public class User4Sbox diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs similarity index 98% rename from v2rayN/ServiceLib/Models/V2rayConfig.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index 10420dade79..c2dd39fe107 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.CoreConfigs; public class V2rayConfig { @@ -86,7 +86,7 @@ public class Inboundsettings4Ray public string? autoOutboundsInterface { get; set; } - // public List? dns { get; set; } + public List? dns { get; set; } } public class UsersItem4Ray @@ -276,6 +276,7 @@ public class BalancersItem4Ray public List? selector { get; set; } public BalancersStrategy4Ray? strategy { get; set; } public string? tag { get; set; } + public string? fallbackTag { get; set; } } public class BalancersStrategy4Ray @@ -372,6 +373,7 @@ public class TlsSettings4Ray public string? spiderX { get; set; } public string? mldsa65Verify { get; set; } public List? certificates { get; set; } + public string? verifyPeerCertByName { get; set; } public string? pinnedPeerCertSha256 { get; set; } public bool? disableSystemRoot { get; set; } public string? echConfigList { get; set; } diff --git a/v2rayN/ServiceLib/Models/V2rayMetricsVars.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayMetricsVars.cs similarity index 88% rename from v2rayN/ServiceLib/Models/V2rayMetricsVars.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/V2rayMetricsVars.cs index cc8d64bd9c6..ed963f42814 100644 --- a/v2rayN/ServiceLib/Models/V2rayMetricsVars.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayMetricsVars.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace ServiceLib.Models; +namespace ServiceLib.Models.CoreConfigs; internal class V2rayMetricsVars { diff --git a/v2rayN/ServiceLib/Models/V2rayTcpRequest.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayTcpRequest.cs similarity index 85% rename from v2rayN/ServiceLib/Models/V2rayTcpRequest.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/V2rayTcpRequest.cs index c08284b45f6..7b231a1385f 100644 --- a/v2rayN/ServiceLib/Models/V2rayTcpRequest.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayTcpRequest.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.CoreConfigs; public class V2rayTcpRequest { diff --git a/v2rayN/ServiceLib/Models/CheckUpdateModel.cs b/v2rayN/ServiceLib/Models/Dto/CheckUpdateModel.cs similarity index 51% rename from v2rayN/ServiceLib/Models/CheckUpdateModel.cs rename to v2rayN/ServiceLib/Models/Dto/CheckUpdateModel.cs index 2707cc6e4ce..bb9b607536e 100644 --- a/v2rayN/ServiceLib/Models/CheckUpdateModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/CheckUpdateModel.cs @@ -1,10 +1,12 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class CheckUpdateModel : ReactiveObject { public bool? IsSelected { get; set; } - public string? CoreType { get; set; } + public ECoreType? CoreType { get; set; } [Reactive] public string? Remarks { get; set; } public string? FileName { get; set; } public bool? IsFinished { get; set; } + public bool IsGeoFile { get; set; } + public string CoreTypeForStorage => IsGeoFile ? "GeoFiles" : (CoreType?.ToString() ?? ""); } diff --git a/v2rayN/ServiceLib/Models/ClashConnectionModel.cs b/v2rayN/ServiceLib/Models/Dto/ClashConnectionModel.cs similarity index 93% rename from v2rayN/ServiceLib/Models/ClashConnectionModel.cs rename to v2rayN/ServiceLib/Models/Dto/ClashConnectionModel.cs index 124118520e6..943f1fd66cf 100644 --- a/v2rayN/ServiceLib/Models/ClashConnectionModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/ClashConnectionModel.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class ClashConnectionModel { diff --git a/v2rayN/ServiceLib/Models/ClashConnections.cs b/v2rayN/ServiceLib/Models/Dto/ClashConnections.cs similarity index 97% rename from v2rayN/ServiceLib/Models/ClashConnections.cs rename to v2rayN/ServiceLib/Models/Dto/ClashConnections.cs index 7ac2bbd2b64..236e2668d81 100644 --- a/v2rayN/ServiceLib/Models/ClashConnections.cs +++ b/v2rayN/ServiceLib/Models/Dto/ClashConnections.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class ClashConnections { diff --git a/v2rayN/ServiceLib/Models/ClashProviders.cs b/v2rayN/ServiceLib/Models/Dto/ClashProviders.cs similarity index 80% rename from v2rayN/ServiceLib/Models/ClashProviders.cs rename to v2rayN/ServiceLib/Models/Dto/ClashProviders.cs index 402add5d0e8..830409062ed 100644 --- a/v2rayN/ServiceLib/Models/ClashProviders.cs +++ b/v2rayN/ServiceLib/Models/Dto/ClashProviders.cs @@ -1,6 +1,6 @@ -using static ServiceLib.Models.ClashProxies; +using static ServiceLib.Models.Dto.ClashProxies; -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class ClashProviders { diff --git a/v2rayN/ServiceLib/Models/ClashProxies.cs b/v2rayN/ServiceLib/Models/Dto/ClashProxies.cs similarity index 94% rename from v2rayN/ServiceLib/Models/ClashProxies.cs rename to v2rayN/ServiceLib/Models/Dto/ClashProxies.cs index 97028b7086f..94986e50297 100644 --- a/v2rayN/ServiceLib/Models/ClashProxies.cs +++ b/v2rayN/ServiceLib/Models/Dto/ClashProxies.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class ClashProxies { diff --git a/v2rayN/ServiceLib/Models/ClashProxyModel.cs b/v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs similarity index 90% rename from v2rayN/ServiceLib/Models/ClashProxyModel.cs rename to v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs index 10d68e14b90..5e460d9fb80 100644 --- a/v2rayN/ServiceLib/Models/ClashProxyModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/ClashProxyModel.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class ClashProxyModel : ReactiveObject diff --git a/v2rayN/ServiceLib/Models/CmdItem.cs b/v2rayN/ServiceLib/Models/Dto/CmdItem.cs similarity index 77% rename from v2rayN/ServiceLib/Models/CmdItem.cs rename to v2rayN/ServiceLib/Models/Dto/CmdItem.cs index a660208e739..7b9813af4a0 100644 --- a/v2rayN/ServiceLib/Models/CmdItem.cs +++ b/v2rayN/ServiceLib/Models/Dto/CmdItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class CmdItem { diff --git a/v2rayN/ServiceLib/Models/ComboItem.cs b/v2rayN/ServiceLib/Models/Dto/ComboItem.cs similarity index 80% rename from v2rayN/ServiceLib/Models/ComboItem.cs rename to v2rayN/ServiceLib/Models/Dto/ComboItem.cs index c092f437f71..fec6fd9d89f 100644 --- a/v2rayN/ServiceLib/Models/ComboItem.cs +++ b/v2rayN/ServiceLib/Models/Dto/ComboItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class ComboItem { diff --git a/v2rayN/ServiceLib/Models/GitHubRelease.cs b/v2rayN/ServiceLib/Models/Dto/GitHubRelease.cs similarity index 98% rename from v2rayN/ServiceLib/Models/GitHubRelease.cs rename to v2rayN/ServiceLib/Models/Dto/GitHubRelease.cs index f6549467385..ed2f48c8c8c 100644 --- a/v2rayN/ServiceLib/Models/GitHubRelease.cs +++ b/v2rayN/ServiceLib/Models/Dto/GitHubRelease.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class GitHubReleaseAsset { diff --git a/v2rayN/ServiceLib/Models/IPAPIInfo.cs b/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs similarity index 64% rename from v2rayN/ServiceLib/Models/IPAPIInfo.cs rename to v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs index 86cfe1112e0..0257ab67b4b 100644 --- a/v2rayN/ServiceLib/Models/IPAPIInfo.cs +++ b/v2rayN/ServiceLib/Models/Dto/IPAPIInfo.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; internal class IPAPIInfo { @@ -17,3 +17,12 @@ public class LocationInfo { public string? country_code { get; set; } } + +public readonly record struct IpInfoResult(string Country, string? Ip) +{ + public override string ToString() + { + var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji(); + return $"{emoji}({Country}) {Ip}"; + } +} diff --git a/v2rayN/ServiceLib/Models/ProfileItemModel.cs b/v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs similarity index 93% rename from v2rayN/ServiceLib/Models/ProfileItemModel.cs rename to v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs index 53170f174dd..7c8b96df874 100644 --- a/v2rayN/ServiceLib/Models/ProfileItemModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/ProfileItemModel.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class ProfileItemModel : ReactiveObject @@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject [Reactive] public string SpeedVal { get; set; } + [Reactive] + public string IpInfo { get; set; } + [Reactive] public string TodayUp { get; set; } diff --git a/v2rayN/ServiceLib/Models/RetResult.cs b/v2rayN/ServiceLib/Models/Dto/RetResult.cs similarity index 93% rename from v2rayN/ServiceLib/Models/RetResult.cs rename to v2rayN/ServiceLib/Models/Dto/RetResult.cs index 688eca3e0f5..3d6c9345311 100644 --- a/v2rayN/ServiceLib/Models/RetResult.cs +++ b/v2rayN/ServiceLib/Models/Dto/RetResult.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class RetResult { diff --git a/v2rayN/ServiceLib/Models/RoutingItemModel.cs b/v2rayN/ServiceLib/Models/Dto/RoutingItemModel.cs similarity index 65% rename from v2rayN/ServiceLib/Models/RoutingItemModel.cs rename to v2rayN/ServiceLib/Models/Dto/RoutingItemModel.cs index 2326f85bf10..2abde9241ed 100644 --- a/v2rayN/ServiceLib/Models/RoutingItemModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/RoutingItemModel.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class RoutingItemModel : RoutingItem diff --git a/v2rayN/ServiceLib/Models/RoutingTemplate.cs b/v2rayN/ServiceLib/Models/Dto/RoutingTemplate.cs similarity index 81% rename from v2rayN/ServiceLib/Models/RoutingTemplate.cs rename to v2rayN/ServiceLib/Models/Dto/RoutingTemplate.cs index 0cacc1bbe27..0d3fd808e61 100644 --- a/v2rayN/ServiceLib/Models/RoutingTemplate.cs +++ b/v2rayN/ServiceLib/Models/Dto/RoutingTemplate.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class RoutingTemplate diff --git a/v2rayN/ServiceLib/Models/RulesItemModel.cs b/v2rayN/ServiceLib/Models/Dto/RulesItemModel.cs similarity index 89% rename from v2rayN/ServiceLib/Models/RulesItemModel.cs rename to v2rayN/ServiceLib/Models/Dto/RulesItemModel.cs index f3eda16bd11..b056f47c753 100644 --- a/v2rayN/ServiceLib/Models/RulesItemModel.cs +++ b/v2rayN/ServiceLib/Models/Dto/RulesItemModel.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class RulesItemModel : RulesItem diff --git a/v2rayN/ServiceLib/Models/SemanticVersion.cs b/v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs similarity index 99% rename from v2rayN/ServiceLib/Models/SemanticVersion.cs rename to v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs index 78463434b6d..1e66353c07f 100644 --- a/v2rayN/ServiceLib/Models/SemanticVersion.cs +++ b/v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class SemanticVersion { diff --git a/v2rayN/ServiceLib/Models/ServerSpeedItem.cs b/v2rayN/ServiceLib/Models/Dto/ServerSpeedItem.cs similarity index 91% rename from v2rayN/ServiceLib/Models/ServerSpeedItem.cs rename to v2rayN/ServiceLib/Models/Dto/ServerSpeedItem.cs index 0a859af6fef..3d9e75624bf 100644 --- a/v2rayN/ServiceLib/Models/ServerSpeedItem.cs +++ b/v2rayN/ServiceLib/Models/Dto/ServerSpeedItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class ServerSpeedItem : ServerStatItem diff --git a/v2rayN/ServiceLib/Models/ServerTestItem.cs b/v2rayN/ServiceLib/Models/Dto/ServerTestItem.cs similarity index 91% rename from v2rayN/ServiceLib/Models/ServerTestItem.cs rename to v2rayN/ServiceLib/Models/Dto/ServerTestItem.cs index 00e26b83c18..66d3325545d 100644 --- a/v2rayN/ServiceLib/Models/ServerTestItem.cs +++ b/v2rayN/ServiceLib/Models/Dto/ServerTestItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class ServerTestItem diff --git a/v2rayN/ServiceLib/Models/SpeedTestResult.cs b/v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs similarity index 69% rename from v2rayN/ServiceLib/Models/SpeedTestResult.cs rename to v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs index 6e0ce70ffa4..2e0ab1a5b26 100644 --- a/v2rayN/ServiceLib/Models/SpeedTestResult.cs +++ b/v2rayN/ServiceLib/Models/Dto/SpeedTestResult.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; [Serializable] public class SpeedTestResult @@ -8,4 +8,6 @@ public class SpeedTestResult public string? Delay { get; set; } public string? Speed { get; set; } + + public string? IpInfo { get; set; } } diff --git a/v2rayN/ServiceLib/Models/SsSIP008.cs b/v2rayN/ServiceLib/Models/Dto/SsSIP008.cs similarity index 91% rename from v2rayN/ServiceLib/Models/SsSIP008.cs rename to v2rayN/ServiceLib/Models/Dto/SsSIP008.cs index 66077471447..d6f05faf205 100644 --- a/v2rayN/ServiceLib/Models/SsSIP008.cs +++ b/v2rayN/ServiceLib/Models/Dto/SsSIP008.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class SsSIP008 { diff --git a/v2rayN/ServiceLib/Models/UpdateResult.cs b/v2rayN/ServiceLib/Models/Dto/UpdateResult.cs similarity index 92% rename from v2rayN/ServiceLib/Models/UpdateResult.cs rename to v2rayN/ServiceLib/Models/Dto/UpdateResult.cs index d8f18dd4211..42a3b352f4f 100644 --- a/v2rayN/ServiceLib/Models/UpdateResult.cs +++ b/v2rayN/ServiceLib/Models/Dto/UpdateResult.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; public class UpdateResult { diff --git a/v2rayN/ServiceLib/Models/VmessQRCode.cs b/v2rayN/ServiceLib/Models/Dto/VmessQRCode.cs similarity index 97% rename from v2rayN/ServiceLib/Models/VmessQRCode.cs rename to v2rayN/ServiceLib/Models/Dto/VmessQRCode.cs index f182c3283c4..c259cf77478 100644 --- a/v2rayN/ServiceLib/Models/VmessQRCode.cs +++ b/v2rayN/ServiceLib/Models/Dto/VmessQRCode.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Dto; /// /// https://github.com/2dust/v2rayN/wiki/ diff --git a/v2rayN/ServiceLib/Models/DNSItem.cs b/v2rayN/ServiceLib/Models/Entities/DNSItem.cs similarity index 92% rename from v2rayN/ServiceLib/Models/DNSItem.cs rename to v2rayN/ServiceLib/Models/Entities/DNSItem.cs index 2dea42d0c39..4bb4f65aa25 100644 --- a/v2rayN/ServiceLib/Models/DNSItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/DNSItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class DNSItem diff --git a/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs b/v2rayN/ServiceLib/Models/Entities/FullConfigTemplateItem.cs similarity index 91% rename from v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs rename to v2rayN/ServiceLib/Models/Entities/FullConfigTemplateItem.cs index b3e3b14e574..c51aa55e03f 100644 --- a/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/FullConfigTemplateItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class FullConfigTemplateItem diff --git a/v2rayN/ServiceLib/Models/ProfileExItem.cs b/v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs similarity index 76% rename from v2rayN/ServiceLib/Models/ProfileExItem.cs rename to v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs index 33b20c57c64..85d1d53674e 100644 --- a/v2rayN/ServiceLib/Models/ProfileExItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProfileExItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class ProfileExItem @@ -10,4 +10,5 @@ public class ProfileExItem public decimal Speed { get; set; } public int Sort { get; set; } public string? Message { get; set; } + public string? IpInfo { get; set; } } diff --git a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/Entities/ProfileGroupItem.cs similarity index 93% rename from v2rayN/ServiceLib/Models/ProfileGroupItem.cs rename to v2rayN/ServiceLib/Models/Entities/ProfileGroupItem.cs index 94a9aad2d8f..6a878a5061c 100644 --- a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProfileGroupItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Obsolete("Use ProtocolExtraItem instead.")] [Serializable] diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs similarity index 98% rename from v2rayN/ServiceLib/Models/ProfileItem.cs rename to v2rayN/ServiceLib/Models/Entities/ProfileItem.cs index ae5f77adafa..94dd3a69b88 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class ProfileItem @@ -191,6 +191,7 @@ public void SetTransportExtra(TransportExtraItem transportExtra) public string Cert { get; set; } public string CertSha { get; set; } public string EchConfigList { get; set; } + public string VerifyPeerCertByName { get; set; } public string Finalmask { get; set; } public string ProtoExtra { get; set; } diff --git a/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs b/v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs similarity index 97% rename from v2rayN/ServiceLib/Models/ProtocolExtraItem.cs rename to v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs index 03403f5b85d..e9916e33866 100644 --- a/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; public record ProtocolExtraItem { diff --git a/v2rayN/ServiceLib/Models/RoutingItem.cs b/v2rayN/ServiceLib/Models/Entities/RoutingItem.cs similarity index 94% rename from v2rayN/ServiceLib/Models/RoutingItem.cs rename to v2rayN/ServiceLib/Models/Entities/RoutingItem.cs index ddd27a9b183..fae611e7024 100644 --- a/v2rayN/ServiceLib/Models/RoutingItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/RoutingItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class RoutingItem diff --git a/v2rayN/ServiceLib/Models/RulesItem.cs b/v2rayN/ServiceLib/Models/Entities/RulesItem.cs similarity index 94% rename from v2rayN/ServiceLib/Models/RulesItem.cs rename to v2rayN/ServiceLib/Models/Entities/RulesItem.cs index 5a5cf52956d..5c7c07d8ce4 100644 --- a/v2rayN/ServiceLib/Models/RulesItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/RulesItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class RulesItem diff --git a/v2rayN/ServiceLib/Models/ServerStatItem.cs b/v2rayN/ServiceLib/Models/Entities/ServerStatItem.cs similarity index 88% rename from v2rayN/ServiceLib/Models/ServerStatItem.cs rename to v2rayN/ServiceLib/Models/Entities/ServerStatItem.cs index 05cd1ee60fd..a693fa4e1eb 100644 --- a/v2rayN/ServiceLib/Models/ServerStatItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ServerStatItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class ServerStatItem diff --git a/v2rayN/ServiceLib/Models/SubItem.cs b/v2rayN/ServiceLib/Models/Entities/SubItem.cs similarity index 94% rename from v2rayN/ServiceLib/Models/SubItem.cs rename to v2rayN/ServiceLib/Models/Entities/SubItem.cs index 612ec15b637..a66eb4ce9ee 100644 --- a/v2rayN/ServiceLib/Models/SubItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/SubItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class SubItem diff --git a/v2rayN/ServiceLib/Models/TransportExtraItem.cs b/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs similarity index 93% rename from v2rayN/ServiceLib/Models/TransportExtraItem.cs rename to v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs index 77fbc984773..b454f0228c6 100644 --- a/v2rayN/ServiceLib/Models/TransportExtraItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; public record TransportExtraItem { diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 7aba3abe305..b7cf06dacb6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -564,6 +564,15 @@ public static string LvTestDelay { } } + /// + /// 查找类似 IP Info 的本地化字符串。 + /// + public static string LvTestIpInfo { + get { + return ResourceManager.GetString("LvTestIpInfo", resourceCulture); + } + } + /// /// 查找类似 Speed (MB/s) 的本地化字符串。 /// @@ -870,6 +879,15 @@ public static string menuBackupAndRestore { } } + /// + /// 查找类似 Only Check 的本地化字符串。 + /// + public static string menuCheckOnly { + get { + return ResourceManager.GetString("menuCheckOnly", resourceCulture); + } + } + /// /// 查找类似 Check Update 的本地化字符串。 /// @@ -1302,6 +1320,15 @@ public static string menuMsgViewSelectAll { } } + /// + /// 查找类似 New Update 的本地化字符串。 + /// + public static string menuNewUpdate { + get { + return ResourceManager.GetString("menuNewUpdate", resourceCulture); + } + } + /// /// 查找类似 Open the storage location 的本地化字符串。 /// @@ -1887,6 +1914,24 @@ public static string menuWebsiteItem { } } + /// + /// 查找类似 Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. 的本地化字符串。 + /// + public static string MsgAllowInsecureDeprecated { + get { + return ResourceManager.GetString("MsgAllowInsecureDeprecated", resourceCulture); + } + } + + /// + /// 查找类似 {0} has a new version available: {1} 的本地化字符串。 + /// + public static string MsgCheckUpdateHasNewVersion { + get { + return ResourceManager.GetString("MsgCheckUpdateHasNewVersion", resourceCulture); + } + } + /// /// 查找类似 Core '{0}' does not support network type '{1}' 的本地化字符串。 /// @@ -2040,6 +2085,15 @@ public static string MsgNeedUrl { } } + /// + /// 查找类似 Not Support 的本地化字符串。 + /// + public static string MsgNotSupport { + get { + return ResourceManager.GetString("MsgNotSupport", resourceCulture); + } + } + /// /// 查找类似 Not support protocol '{0}' 的本地化字符串。 /// @@ -2175,6 +2229,15 @@ public static string MsgSubscriptionPrevProfileNotFound { } } + /// + /// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。 + /// + public static string MsgTunRouteExcludeInvalidAddress { + get { + return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture); + } + } + /// /// 查找类似 Unpacking... 的本地化字符串。 /// @@ -3484,7 +3547,25 @@ public static string TbRoundRobin { } /// - /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 + /// 查找类似 Route Exclude Address 的本地化字符串。 + /// + public static string TbRouteExcludeAddress { + get { + return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture); + } + } + + /// + /// 查找类似 Use commas (,) to separate. 的本地化字符串。 + /// + public static string TbRouteExcludeAddressTip { + get { + return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture); + } + } + + /// + /// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// public static string TbRoutingInboundTagTips { get { @@ -3718,7 +3799,7 @@ public static string TbSettingsBindInterface { } /// - /// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode 的本地化字符串。 + /// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。 /// public static string TbSettingsBindInterfaceTip { get { @@ -3925,7 +4006,7 @@ public static string TbSettingsEnableCacheFile4Sbox { } /// - /// 查找类似 Check for pre-release updates 的本地化字符串。 + /// 查找类似 Check for pre-release 的本地化字符串。 /// public static string TbSettingsEnableCheckPreReleaseUpdate { get { @@ -4653,6 +4734,15 @@ public static string TbValidateDirectExpectedIPsDesc { } } + /// + /// 查找类似 Verify Peer Cert By Name 的本地化字符串。 + /// + public static string TbVerifyPeerCertByName { + get { + return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture); + } + } + /// /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 21a6c7f1d5a..246417a6086 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1723,7 +1723,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Bind Interface - For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode + For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode PreSharedKey @@ -1731,4 +1731,19 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Export v2rayN Internal Share Link to Clipboard + + {0} has a new version available: {1} + + + Only Check + + + Not Support + + + IP Info + + + New Update + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index bca6c2b7c47..b97dfcb5a5e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1675,7 +1675,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Ajouter [NaïveProxy] - Insecure Concurrency + Concurrence non sûre Nom d’utilisateur @@ -1699,10 +1699,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Allow insecure cert fetch (self-signed) - Only for fetching self-signed certificates. This may expose you to MITM risks. + Pour obtenir des certificats auto-signés uniquement. Risque MITM. - Adresse sortante locale (SendThrough) + Adresse de sortie locale (SendThrough) Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale @@ -1717,15 +1717,30 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN - Test Configurations UDP Delay + Tester la latence UDP des profils - UDP Test Url + URL de test UDP PreSharedKey - Export v2rayN Internal Share Link to Clipboard + Exporter le lien interne v2rayN vers le presse-papiers + + + {0} a une nouvelle version disponible: {1} + + + Uniquement vérifier + + + Non supporté + + + Détails IP + + + Nouvelle MAJ \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index d73a170c219..f1e159c3b45 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1723,7 +1723,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Bind Interface - For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode + For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode PreSharedKey @@ -1731,4 +1731,19 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Export v2rayN Internal Share Link to Clipboard + + {0} has a new version available: {1} + + + Only Check + + + Not Support + + + IP Info + + + New Update + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 13883be46fe..ec97d10a905 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -691,7 +691,7 @@ Automatically adjust column width after subscription update - Check for pre-release updates + Check for pre-release Exception @@ -1336,7 +1336,7 @@ Enable second mixed port - socks: local port, socks2: second local port, socks3: LAN port + tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port Theme @@ -1581,6 +1581,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchConfigList + + Verify Peer Cert By Name + Full certificate (chain), PEM format @@ -1723,7 +1726,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Bind Interface - For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode + For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode PreSharedKey @@ -1731,4 +1734,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Export v2rayN Internal Share Link to Clipboard + + {0} has a new version available: {1} + + + Only Check + + + Not Support + + + IP Info + + + New Update + + + Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. + + + Route Exclude Address + + + Use commas (,) to separate. + + + Invalid address in TUN route exclude list: {0} + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 4d02c0a70c9..f74e45c28ca 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -121,7 +121,7 @@ Ссылка успешно скопирована в буфер обмена - Сначала проверьте настройки сервера + Недопустимая конфигурация, проверьте или выберите заново Недопустимый формат конфигурации @@ -166,7 +166,7 @@ Некорректная конфигурация. Проверьте - Исходная конфигурация + Инициализация конфигурации {0} {1} является последней версией @@ -256,7 +256,7 @@ Операция не удалась, проверьте и повторите попытку - Введите примечания + Введите псевдоним Выберите метод шифрования @@ -307,7 +307,7 @@ {0}: одно из обязательных полей - Примечания + Псевдоним URL (необязательно) @@ -334,13 +334,13 @@ Введите корректный пользовательский DNS - *Путь ws/http upgrade/xhttp + *Путь ws/httpupgrade/xhttp *HTTP2-путь - *QUIC-ключ / KCP-seed + *Ключ шифрования QUIC Имя сервиса gRPC @@ -349,13 +349,13 @@ *http-хосты, разделённые запятыми (,) - *Хост ws/http upgrade/xhttp + *Хост ws/httpupgrade/xhttp *HTTP2-хосты, разделённые запятыми (,) - Безопасность *QUIC + *Метод шифрования QUIC Тип raw-камуфляжа @@ -400,7 +400,7 @@ Фильтр, нажмите Enter для выполнения - Проверить обновления + Обновить Закрыть @@ -415,13 +415,13 @@ Помощь - Настройки + Параметры Продвижение - Перезагрузить + Перезапустить службу Настройки маршрутизации @@ -445,10 +445,10 @@ Настройки групп подписки - Обновить подписку без прокси + Обновить все подписки без прокси - Обновить подписку с прокси + Обновить все подписки через прокси Системный прокси @@ -562,7 +562,7 @@ Сортировка - User-Agent + User-Agent (необязательно) Отмена @@ -577,7 +577,7 @@ Адрес - Разрешить небезопасные + Пропустить проверку сертификата (allowInsecure) ALPN @@ -616,10 +616,10 @@ TLS - *По-умолчанию raw + *По умолчанию raw; при неверном выборе подключение будет невозможно - Ядро + Тип ядра Управление потоком (Flow) @@ -640,7 +640,7 @@ Шифрование - Пользователь (необязательно) + Имя пользователя (необязательно) Шифрование @@ -649,7 +649,7 @@ Порт SOCKS - * После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости + *Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций Обзор @@ -682,7 +682,7 @@ Настройки типа ядра - Разрешить небезопасные + По умолчанию пропускать проверку сертификата (allowInsecure) «Freedom»: стратегия обработки доменов исходящего трафика @@ -703,7 +703,7 @@ Показывать скорость в реальном времени (требуется перезапуск) - Сохранить старые при удалении дублей + При удалении дубликатов сохранять элементы с меньшим порядковым номером Записывать логи @@ -730,13 +730,13 @@ Смешанный порт - Запускать при старте системы + Запускать при старте системы (может не сработать) Включить статистику трафика (требуется перезапуск) - URL конвертации подписок + URL конвертации подписок (необязательно) Настройки системного прокси @@ -787,13 +787,13 @@ Запущено от имени администратора - Спуститься вниз + Переместить в самый низ Вниз - Подняться наверх + Переместить в самый верх Вверх @@ -805,7 +805,7 @@ Веб-сайт {0} - Добавить + Добавить набор правил Импортировать правила @@ -841,7 +841,7 @@ Добавить правило - Экспорт выделенных правил + Экспортировать выбранные правила в буфер обмена Список правил @@ -859,7 +859,7 @@ Документация RuleObject - Поддерживаются DNS-объекты, нажмите для просмотра документации + Заполните DNS-объект (формат JSON), нажмите для просмотра документации Для группы оставьте это поле пустым @@ -871,13 +871,13 @@ Системные прокси изменены - Только маршрут + Только для маршрутизации (routeOnly) Не использовать прокси для локальных (интранет) адресов - Тест задержки и скорости всех серверов (Ctrl+E) + Многопоточный тест задержки и скорости (Ctrl+E) Задержка (мс) @@ -886,10 +886,10 @@ Скорость (МБ/с) - Не удалось запустить ядро, посмотрите логи + Не удалось запустить ядро, см. сообщение - Фильтр по примечаниям (регулярные выражения) + Фильтр по псевдониму (регулярные выражения) Отображать журнал @@ -937,7 +937,7 @@ Шрифт (требуется перезапуск) - Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек + Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5; @@ -1009,7 +1009,7 @@ Пользовательский DNS для sing-box - Заполните структуру DNS, нажмите, чтобы открыть документ + Заполните структуру DNS (формат JSON), нажмите для просмотра документации Нажмите, чтобы импортировать конфигурацию DNS по умолчанию @@ -1033,7 +1033,7 @@ Добавить сервер [Hysteria2] - Максимальная пропускная способность Hysteria (загрузка/отдача) + Максимальная пропускная способность Hysteria (отдача/загрузка) Использовать системный файл hosts @@ -1045,13 +1045,13 @@ Управление перегрузками - Примечания к предыдущему прокси + Псевдоним предыдущего прокси - Примечания к следующему прокси + Псевдоним следующего прокси - Убедитесь, что примечание существует и является уникальным + Убедитесь, что псевдоним существует и является уникальным Автоматическая маршрутизация @@ -1177,7 +1177,7 @@ Глобальный режим - Не менять + Как в исходной конфигурации Правила @@ -1210,7 +1210,7 @@ Экспорт ссылок в формате Base64 в буфер обмена - Экспортировать полную конфигурацию в буфер обмена + Экспортировать выбранную полную конфигурацию в буфер обмена Показать или скрыть главное окно @@ -1300,7 +1300,7 @@ Не используйте небезопасный адрес подписки по протоколу HTTP - Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки + Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение Вы уверены, что хотите выйти? @@ -1318,13 +1318,13 @@ XHTTP-режим - Сырой JSON, формат: { XHTTP Object } + Сырой JSON, формат: { XHTTPObject } Сворачивать в трей при закрытии окна - Количество одновременно выполняемых тестов при многоэтапном тестировании + Количество параллельных задач при многопоточном тестировании Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,) @@ -1336,7 +1336,7 @@ Включить второй смешанный порт - socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт + tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт Тема @@ -1357,7 +1357,7 @@ Удалено {0} недействительных - Диапазон портов сервера + Диапазон портов для переключения (Port Hopping) Заменит указанный порт, перечисляйте через запятую (,) @@ -1369,7 +1369,7 @@ URL для тестирования текущего соединения - Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально + Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален Неверный пароль, попробуйте ещё раз. @@ -1384,7 +1384,7 @@ Удалённый DNS - Внутренний DNS + DNS для прямых подключений Стратегия разрешения прямых соединений @@ -1489,13 +1489,13 @@ Тип группы политик - Добавить группу политик + Добавить группу политик Добавить цепочку прокси - Добавить дочернюю конфигурацию + Добавить дочернюю конфигурацию Удалить дочернюю конфигурацию @@ -1534,7 +1534,7 @@ Bootstrap DNS - Разрешает домены DNS-серверов, требуется IP-адрес + Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес Тест реальной задержки @@ -1547,7 +1547,7 @@ Привязанный сертификат (заполните любое из полей) -При указании сертификат будет привязан, а «Разрешить небезопасные» отключится. +При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится. Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе. @@ -1726,9 +1726,24 @@ Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN - PreSharedKey + Общий ключ (PSK) - Export v2rayN Internal Share Link to Clipboard + Экспорт внутренней ссылки v2rayN в буфер обмена + + + Доступна новая версия {0}: {1} + + + Проверить + + + Не поддерживается + + + Информация об IP + + + Доступно обновление \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index e86171606c1..4654b723948 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -691,7 +691,7 @@ 自动调整配置列宽在更新订阅后 - 检查 Pre-Release 更新 (请谨慎启用) + 检查 Pre-Release 例外 @@ -1333,7 +1333,7 @@ 开启第二个本地监听端口 - Socks:本地端口,Socks2:第二个本地端口,Socks3:局域网端口 + Tun:TUN 入站,Socks:本地端口,Socks2:第二个本地端口,Socks3:局域网端口 主题 @@ -1720,7 +1720,7 @@ 绑定网口 - 用于多网口环境,填写要绑定的网口名称,仅生效于 Windows 系统和 TUN 模式 + 用于多网口环境,填写要绑定的网口名称,仅生效于 Windows 系统或 TUN 模式 PreSharedKey @@ -1728,4 +1728,34 @@ 导出 v2rayN 内部分享链接至剪贴板 (多选) + + {0} 有新版本可用:{1} + + + 仅检查 + + + 不支持 + + + IP 信息 + + + 有更新 + + + 警告:Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 + + + Verify Peer Cert By Name + + + 路由排除地址 + + + 使用逗号 (,) 分隔 + + + TUN 路由排除列表包含无效地址:{0} + \ 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 61f231b14ed..2f1c1857cf6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -691,7 +691,7 @@ 在更新訂閱後自動調整列寬 - 檢查 Pre-Release 更新 (請謹慎啟用) + 檢查 Pre-Release 例外 @@ -1720,7 +1720,7 @@ 綁定網路介面 - 適用於多網路介面環境,請填寫要綁定的介面名稱;Windows 系統有效,其他系統僅在 TUN 模式下生效。 + 適用於多網路介面環境,請填寫要綁定的介面名稱;Windows 系統有效,其他系統僅在 TUN 模式下生效 PreSharedKey @@ -1728,4 +1728,31 @@ 匯出 v2rayN 內部分享連結至剪貼簿(多選) - + + {0} 有新版本可用:{1} + + + 僅檢查 + + + 不支援 + + + IP 資訊 + + + 有更新 + + + 警告:Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。 + + + Verify Peer Cert By Name + + + 路由排除位址 + + + 使用逗號 (,) 分隔 + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SampleTunInbound b/v2rayN/ServiceLib/Sample/SampleTunInbound index 95cefbc18a1..2edc5ea0643 100644 --- a/v2rayN/ServiceLib/Sample/SampleTunInbound +++ b/v2rayN/ServiceLib/Sample/SampleTunInbound @@ -8,6 +8,9 @@ "172.18.0.1/30", "fdfe:dcba:9876::1/126" ], + "dns": [ + "172.18.0.1" + ], "autoSystemRoutingTable": [ "0.0.0.0/0", "::/0" diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_inbound b/v2rayN/ServiceLib/Sample/tun_singbox_inbound index db5b0b32e0e..b23346c161e 100644 --- a/v2rayN/ServiceLib/Sample/tun_singbox_inbound +++ b/v2rayN/ServiceLib/Sample/tun_singbox_inbound @@ -1,6 +1,6 @@ { "type": "tun", - "tag": "tun-in", + "tag": "tun", "interface_name": "singbox_tun", "address": [ "172.18.0.1/30", diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index 60d9c62276e..5b0b807261b 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -6,11 +6,13 @@ + true - + + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs index 2e13c624b25..3aa06d4e058 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs @@ -71,6 +71,7 @@ private void GenInbounds() { tunInbound.address = ["172.18.0.1/30"]; } + tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress; _coreConfig.inbounds.Add(tunInbound); } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 2b6bdf2b647..b39312ebd0c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -87,8 +87,14 @@ private void GenRouting() }); _coreConfig.route.rules.Add(new() { - protocol = ["dns"], - action = "hijack-dns" + type = "logical", + mode = "or", + action = "hijack-dns", + rules = + [ + new() { port = [53] }, + new() { protocol = ["dns"] }, + ], }); } else @@ -96,7 +102,7 @@ private void GenRouting() _coreConfig.route.rules.Add(new() { port = [53], - action = "hijack-dns" + action = "hijack-dns", }); } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs index f294de460e2..78050667332 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -104,6 +104,7 @@ private void GenBalancer(EMultipleLoad multipleLoad, string selector = Global.Pr }, }, tag = balancerTag, + fallbackTag = multipleLoad == EMultipleLoad.Fallback ? Global.DirectTag : null, }; _coreConfig.routing.balancers ??= new(); _coreConfig.routing.balancers.Add(balancer); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 91c8c133928..7f950a83b32 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -10,9 +10,9 @@ private void GenInbounds() var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); _coreConfig.inbounds = []; var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true); + var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort; - if (!context.IsTunEnabled - || (context.IsTunEnabled && _node.Address != Global.Loopback && _node.Port != listenPort)) + if (!context.IsTunEnabled || !isUsingLocalMixedPort) { _coreConfig.inbounds.Add(inbound); @@ -30,14 +30,19 @@ private void GenInbounds() inbound3.listen = listen; _coreConfig.inbounds.Add(inbound3); - //auth + // auth if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) { inbound3.settings.auth = "password"; - inbound3.settings.accounts = new List - { - new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } - }; + inbound3.settings.accounts = + [ + new() + { + user = _config.Inbound.First().User, + pass = _config.Inbound.First().Pass, + }, + + ]; } } else @@ -53,12 +58,15 @@ private void GenInbounds() { _config.TunModeItem.Mtu = Global.TunMtus.First(); } - var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { }; + var tunInbound = + JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? + new Inbounds4Ray(); tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun"; tunInbound.settings.MTU = _config.TunModeItem.Mtu; - if (_config.TunModeItem.EnableIPv6Address == false) + if (!_config.TunModeItem.EnableIPv6Address) { tunInbound.settings.gateway = ["172.18.0.1/30"]; + tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"]; } var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); if (!bindInterface.IsNullOrEmpty()) @@ -66,6 +74,53 @@ private void GenInbounds() tunInbound.settings.autoOutboundsInterface = bindInterface; } tunInbound.sniffing = inbound.sniffing; + + if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 }) + { + var wholeInternet = IPNetwork2.Parse("0.0.0.0/0"); + var wholeInternetV6 = IPNetwork2.Parse("::/0"); + + var excludeList = _config.TunModeItem.RouteExcludeAddress.Select(IPNetwork2.Parse) + .Where(x => x != null).ToList(); + + var includeList = new List { wholeInternet }; + var includeListV6 = new List { wholeInternetV6 }; + + foreach (var exclude in excludeList) + { + var temp = new List(); + if (exclude.AddressFamily == AddressFamily.InterNetwork) + { + foreach (var net in includeList) + { + temp.AddRange(net.Subtract(exclude)); + } + includeList = temp; + } + else if (exclude.AddressFamily == AddressFamily.InterNetworkV6) + { + foreach (var net in includeListV6) + { + temp.AddRange(net.Subtract(exclude)); + } + includeListV6 = temp; + } + } + + includeList = IPNetwork2.Supernet(includeList.ToArray()).ToList(); + includeListV6 = IPNetwork2.Supernet(includeListV6.ToArray()).ToList(); + + if (_config.TunModeItem.EnableIPv6Address) + { + tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()) + .Concat(includeListV6.Select(x => x.ToString())).ToList(); + } + else + { + tunInbound.settings.autoSystemRoutingTable = includeList.Select(x => x.ToString()).ToList(); + } + } + _coreConfig.inbounds.Add(tunInbound); } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index f383696090f..beb0aa5b197 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -384,6 +384,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) alpn = _node.GetAlpn(), fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint, echConfigList = _node.EchConfigList.NullIfEmpty(), + verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(), }; if (sni.IsNotEmpty()) { @@ -491,7 +492,6 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) //ws case nameof(ETransport.ws): WsSettings4Ray wsSettings = new(); - wsSettings.headers = new Headers4Ray(); if (host.IsNotEmpty()) { @@ -503,6 +503,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) } if (useragent.IsNotEmpty()) { + wsSettings.headers ??= new Headers4Ray(); wsSettings.headers.UserAgent = useragent; } streamSettings.wsSettings = wsSettings; @@ -522,6 +523,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) } if (useragent.IsNotEmpty()) { + httpupgradeSettings.headers ??= new Headers4Ray(); httpupgradeSettings.headers.UserAgent = useragent; } streamSettings.httpupgradeSettings = httpupgradeSettings; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs index 4f7431fc71c..c3bcf61643c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -27,6 +27,7 @@ private void GenRouting() }); _coreConfig.routing.rules.Add(new() { + inboundTag = ["tun"], port = "53", outboundTag = Global.DnsOutboundTag, }); diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs index 77d3a7c162a..1dbdfae243e 100644 --- a/v2rayN/ServiceLib/Services/DownloadService.cs +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -3,7 +3,7 @@ namespace ServiceLib.Services; /// -///Download +/// Download /// public class DownloadService { @@ -13,7 +13,10 @@ public class DownloadService private static readonly string _tag = "DownloadService"; - public async Task DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func updateFunc) + /// + /// Downloads data with the specified proxy and reports progress messages. + /// + public async Task DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func updateFunc) { try { @@ -36,6 +39,9 @@ await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy, return 0; } + /// + /// Downloads a file and reports progress through events. + /// public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout) { try @@ -64,6 +70,9 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, } } + /// + /// Gets redirect target URL without following redirects automatically. + /// public async Task UrlRedirectAsync(string url, bool blProxy) { var webRequestHandler = new SocketsHttpHandler @@ -86,11 +95,23 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, } } + /// + /// Tries to download string content using proxy switch setting. + /// public async Task TryDownloadString(string url, bool blProxy, string userAgent) + { + var webProxy = await GetWebProxy(blProxy); + return await TryDownloadString(url, webProxy, userAgent); + } + + /// + /// Tries to download string content with a specified proxy. + /// + public async Task TryDownloadString(string url, IWebProxy? webProxy, string userAgent) { try { - var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15); + var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15); if (result1.IsNotEmpty()) { return result1; @@ -108,7 +129,7 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, try { - var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15); + var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15); if (result2.IsNotEmpty()) { return result2; @@ -128,14 +149,12 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, } /// - /// DownloadString + /// Downloads string content via HttpClient. /// - /// - private async Task DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout) + private async Task DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout) { try { - var webProxy = await GetWebProxy(blProxy); var client = new HttpClient(new SocketsHttpHandler() { Proxy = webProxy, @@ -172,15 +191,12 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, } /// - /// DownloadString + /// Downloads string content via DownloaderHelper. /// - /// - private async Task DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout) + private async Task DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout) { try { - var webProxy = await GetWebProxy(blProxy); - if (userAgent.IsNullOrEmpty()) { userAgent = Utils.GetVersion(false); @@ -200,6 +216,9 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, return null; } + /// + /// Creates local SOCKS proxy when proxy switch is enabled. + /// private async Task GetWebProxy(bool blProxy) { if (!blProxy) @@ -215,6 +234,9 @@ await DownloaderHelper.Instance.DownloadFileAsync(webProxy, return new WebProxy($"socks5://{Global.Loopback}:{port}"); } + /// + /// Checks whether the specified TCP endpoint is reachable. + /// private async Task SocketCheck(string ip, int port) { try diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index 251522bacab..11778b22016 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -8,6 +8,8 @@ public class SpeedtestService(Config config, Func updateF private readonly Config? _config = config; private readonly Func? _updateFunc = updateFunc; private static readonly ConcurrentBag _lstExitLoop = new(); + private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize; + private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1); public void RunLoop(ESpeedActionType actionType, List selecteds) { @@ -135,32 +137,39 @@ private async Task> GetClearItem(ESpeedActionType actionTyp private async Task RunTcpingAsync(List selecteds) { - List tasks = []; - foreach (var it in selecteds) + var pageSize = Math.Min(selecteds.Count, _speedTestPageSize); + var lstBatch = GetTestBatchItem(selecteds, pageSize); + + foreach (var lst in lstBatch) { - tasks.Add(Task.Run(async () => + List tasks = []; + foreach (var it in lst) { - try + tasks.Add(Task.Run(async () => { - var responseTime = await GetTcpingTime(it.Address, it.Port); + try + { + var responseTime = await GetTcpingTime(it.Address, it.Port); - ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); - await UpdateFunc(it.IndexId, responseTime.ToString()); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - })); + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); + await UpdateFunc(it.IndexId, responseTime.ToString()); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + })); + } + await Task.WhenAll(tasks); + await Task.Delay(_delayInterval); } - await Task.WhenAll(tasks); } private async Task RunRealPingBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0) { if (pageSize <= 0) { - pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize; + pageSize = Math.Min(lstSelected.Count, _speedTestPageSize); } var lstTest = GetTestBatchItem(lstSelected, pageSize); @@ -172,7 +181,7 @@ private async Task RunRealPingBatchAsync(List lstSelected, strin { lstFailed.AddRange(lst); } - await Task.Delay(100); + await Task.Delay(_delayInterval); } //Retest the failed part @@ -249,7 +258,7 @@ private async Task RunUdpTestBatchAsync(List lstSelected, string { if (pageSize <= 0) { - pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize; + pageSize = Math.Min(lstSelected.Count, _speedTestPageSize); } var lstTest = GetTestBatchItem(lstSelected, pageSize); @@ -261,7 +270,7 @@ private async Task RunUdpTestBatchAsync(List lstSelected, string { lstFailed.AddRange(lst); } - await Task.Delay(100); + await Task.Delay(_delayInterval); } //Retest the failed part @@ -392,10 +401,23 @@ private async Task RunMixedTestAsync(List selecteds, int concurr private async Task DoRealPing(ServerTestItem it) { var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); - var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); + var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); + + if (responseTime > 0) + { + var ipInfo = await ConnectionHandler.GetIPInfo(webProxy); + var ipStr = ipInfo?.ToString() ?? Global.None; + ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr); + await UpdateIpInfoFunc(it.IndexId, ipStr); + } + else + { + await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip); + } + return responseTime; } @@ -491,4 +513,9 @@ private async Task UpdateFunc(string indexId, string delay, string speed = "") ProfileExManager.Instance.SetTestMessage(indexId, speed); } } + + private async Task UpdateIpInfoFunc(string indexId, string ip) + { + await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip }); + } } diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index 57f9d14b348..cc2cefab9fa 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -100,6 +100,43 @@ public async Task CheckUpdateCore(ECoreType type, bool preRelease) } } + public async Task CheckHasUpdateOnly(ECoreType type, bool preRelease) + { + if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type)) + { + return new UpdateResult(false, ResUI.MsgNotSupport); + } + + var downloadHandle = new DownloadService(); + var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease); + return await CheckUpdateAsync(downloadHandle, type, checkPreRelease); + } + + public async Task> CheckHasUpdateOnlyAll(bool preRelease) + { + var msgs = new List(); + foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes()) + { + if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true)) + { + continue; + } + + var result = await CheckHasUpdateOnly(type, preRelease); + if (result.Success && result.Version != null) + { + var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version); + msgs.Add(msg); + AppManager.Instance.SetLastCheckUpdateResult(type, msg); + } + else + { + AppManager.Instance.SetLastCheckUpdateResult(type, result.Msg); + } + } + return msgs; + } + public async Task UpdateGeoFileAll() { await UpdateGeoFiles(); @@ -301,14 +338,11 @@ private async Task ParseDownloadUrl(ECoreType type, UpdateResult r } else if (Utils.IsLinux()) { - var arch = RuntimeInformation.ProcessArchitecture; - if (arch.ToString().Equals("RiscV64", StringComparison.OrdinalIgnoreCase)) - { - return coreInfo?.DownloadUrlLinuxRiscV64; - } - return arch switch + return RuntimeInformation.ProcessArchitecture switch { Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64, + Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64, + Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64, Architecture.X64 => coreInfo?.DownloadUrlLinux64, _ => null, }; diff --git a/v2rayN/ServiceLib/Services/WindowsJobService.cs b/v2rayN/ServiceLib/Services/WindowsJobService.cs index ffd4a36a822..5bc8f276702 100644 --- a/v2rayN/ServiceLib/Services/WindowsJobService.cs +++ b/v2rayN/ServiceLib/Services/WindowsJobService.cs @@ -3,6 +3,7 @@ namespace ServiceLib.Services; /// /// http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net /// +[SupportedOSPlatform("windows")] public sealed class WindowsJobService : IDisposable { private nint handle = nint.Zero; diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 169e8a4424d..f5906f2159d 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -464,7 +464,9 @@ private async Task FetchCert() domain += $":{SelectedSource.Port}"; } - (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch); + (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, + verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), + allowInsecure: AllowInsecureCertFetch); UpdateCertTip(certError); } @@ -489,7 +491,9 @@ private async Task FetchCertChain() domain += $":{SelectedSource.Port}"; } - var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, allowInsecure: AllowInsecureCertFetch); + var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, + verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), + allowInsecure: AllowInsecureCertFetch); Cert = CertPemManager.ConcatenatePemChain(certs); UpdateCertTip(certError); } diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index dc31b41ba29..e2a8a3ce195 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -3,12 +3,13 @@ namespace ServiceLib.ViewModels; public class CheckUpdateViewModel : MyReactiveObject { private const string _geo = "GeoFiles"; - private readonly string _v2rayN = ECoreType.v2rayN.ToString(); + private readonly ECoreType _v2rayN = ECoreType.v2rayN; private List _lstUpdated = []; private static readonly string _tag = "CheckUpdateViewModel"; public IObservableCollection CheckUpdateModels { get; } = new ObservableCollectionExtended(); public ReactiveCommand CheckUpdateCmd { get; } + public ReactiveCommand CheckOnlyCmd { get; } [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } public CheckUpdateViewModel(Func>? updateView) @@ -23,12 +24,19 @@ public CheckUpdateViewModel(Func>? updateView) _ = UpdateView(_v2rayN, ex.Message); }); + CheckOnlyCmd = ReactiveCommand.CreateFromTask(CheckOnly); + CheckOnlyCmd.ThrownExceptions.Subscribe(ex => + { + Logging.SaveLog(_tag, ex); + _ = UpdateView(_v2rayN, ex.Message); + }); + EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate; this.WhenAnyValue( x => x.EnableCheckPreReleaseUpdate, y => y == true) - .Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate); + .Subscribe(c => _ = OnCheckPreReleaseUpdateChanged()); RefreshCheckUpdateItems(); } @@ -37,21 +45,15 @@ private void RefreshCheckUpdateItems() { CheckUpdateModels.Clear(); - if (RuntimeInformation.ProcessArchitecture != Architecture.X86) + foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes()) { - CheckUpdateModels.Add(GetCheckUpdateModel(_v2rayN)); - //Not Windows and under Win10 - if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)) - { - CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.Xray.ToString())); - CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString())); - CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString())); - } + CheckUpdateModels.Add(GetCheckUpdateModel(type)); } - CheckUpdateModels.Add(GetCheckUpdateModel(_geo)); + + CheckUpdateModels.Add(GetGeoFileCheckUpdateModel()); } - private CheckUpdateModel GetCheckUpdateModel(string coreType) + private CheckUpdateModel GetCheckUpdateModel(ECoreType coreType) { if (coreType == _v2rayN && Utils.IsPackagedInstall()) { @@ -59,34 +61,112 @@ private CheckUpdateModel GetCheckUpdateModel(string coreType) { IsSelected = false, CoreType = coreType, - Remarks = ResUI.menuCheckUpdate + " (Not Support)", + IsGeoFile = false, + Remarks = ResUI.menuCheckUpdate + $" ({ResUI.MsgNotSupport})", }; } + AppManager.Instance.LastCheckUpdateResults.TryGetValue(coreType, out var lastResult); return new() { - IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true, + IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType.ToString()) ?? true, CoreType = coreType, + IsGeoFile = false, + Remarks = lastResult ?? ResUI.menuCheckUpdate, + }; + } + + private CheckUpdateModel GetGeoFileCheckUpdateModel() + { + return new() + { + IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(_geo) ?? true, + CoreType = null, + IsGeoFile = true, Remarks = ResUI.menuCheckUpdate, }; } + private async Task OnCheckPreReleaseUpdateChanged() + { + if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate) + { + return; + } + _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate; + await SaveSelectedCoreTypes(); + } + private async Task SaveSelectedCoreTypes() { - _config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList(); + _config.CheckUpdateItem.SelectedCoreTypes = + CheckUpdateModels.Where(t => t.IsSelected == true) + .Select(t => t.CoreTypeForStorage) + .ToList(); + await ConfigHandler.SaveConfig(_config); } + private async Task CheckOnly() + { + await Task.Run(CheckOnlyTask); + } + private async Task CheckUpdate() { await Task.Run(CheckUpdateTask); } + private async Task CheckOnlyTask() + { + await SaveSelectedCoreTypes(); + + for (var k = CheckUpdateModels.Count - 1; k >= 0; k--) + { + var item = CheckUpdateModels[k]; + if (item.IsSelected != true) + { + continue; + } + + await UpdateView(item.CoreType, "..."); + + if (item.IsGeoFile || item.CoreType == null) + { + await UpdateView(item.CoreType, ResUI.menuCheckOnly + $" ({ResUI.MsgNotSupport})"); + continue; + } + + if (item.CoreType == null) + { + await UpdateView(item.CoreType, ResUI.MsgNotSupport); + continue; + } + + var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask); + var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, EnableCheckPreReleaseUpdate); + if (result.Success && result.Version != null) + { + await UpdateView(item.CoreType, string.Format(ResUI.MsgCheckUpdateHasNewVersion, item.CoreType, result.Version)); + } + else + { + await UpdateView(item.CoreType, result.Msg); + } + } + } + private async Task CheckUpdateTask() { _lstUpdated.Clear(); - _lstUpdated = CheckUpdateModels.Where(x => x.IsSelected == true) - .Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList(); + _lstUpdated = CheckUpdateModels + .Where(x => x.IsSelected == true) + .Select(x => new CheckUpdateModel() + { + CoreType = x.CoreType, + IsGeoFile = x.IsGeoFile + }) + .ToList(); await SaveSelectedCoreTypes(); for (var k = CheckUpdateModels.Count - 1; k >= 0; k--) @@ -98,7 +178,8 @@ private async Task CheckUpdateTask() } await UpdateView(item.CoreType, "..."); - if (item.CoreType == _geo) + + if (item.IsGeoFile) { await CheckUpdateGeo(); } @@ -106,16 +187,16 @@ private async Task CheckUpdateTask() { if (Utils.IsPackagedInstall()) { - await UpdateView(_v2rayN, "Not Support"); + await UpdateView(_v2rayN, ResUI.MsgNotSupport); continue; } await CheckUpdateN(EnableCheckPreReleaseUpdate); } - else if (item.CoreType == ECoreType.Xray.ToString()) + else if (item.CoreType == ECoreType.Xray) { await CheckUpdateCore(item, EnableCheckPreReleaseUpdate); } - else + else if (item.CoreType.HasValue) { await CheckUpdateCore(item, false); } @@ -124,7 +205,7 @@ private async Task CheckUpdateTask() await UpdateFinished(); } - private void UpdatedPlusPlus(string coreType, string fileName) + private void UpdatedPlusPlus(ECoreType? coreType, string fileName) { var item = _lstUpdated.FirstOrDefault(x => x.CoreType == coreType); if (item == null) @@ -142,14 +223,14 @@ private async Task CheckUpdateGeo() { async Task _updateUI(bool success, string msg) { - await UpdateView(_geo, msg); + await UpdateView(null, msg); if (success) { - UpdatedPlusPlus(_geo, ""); + UpdatedPlusPlus(null, ""); } } await new UpdateService(_config, _updateUI).UpdateGeoFileAll() - .ContinueWith(t => UpdatedPlusPlus(_geo, "")); + .ContinueWith(t => UpdatedPlusPlus(null, "")); } private async Task CheckUpdateN(bool preRelease) @@ -175,13 +256,15 @@ async Task _updateUI(bool success, string msg) if (success) { await UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore); - UpdatedPlusPlus(model.CoreType, msg); } } - var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType); - await new UpdateService(_config, _updateUI).CheckUpdateCore(type, preRelease) - .ContinueWith(t => UpdatedPlusPlus(model.CoreType, "")); + + if (model.CoreType.HasValue) + { + await new UpdateService(_config, _updateUI).CheckUpdateCore(model.CoreType.Value, preRelease) + .ContinueWith(t => UpdatedPlusPlus(model.CoreType, "")); + } } private async Task UpdateFinished() @@ -257,7 +340,7 @@ private async Task UpgradeCore() { foreach (var item in _lstUpdated) { - if (item.FileName.IsNullOrEmpty()) + if (item.FileName.IsNullOrEmpty() || item.IsGeoFile) { continue; } @@ -267,7 +350,9 @@ private async Task UpgradeCore() { continue; } - var toPath = Utils.GetBinPath("", item.CoreType); + + var coreTypeStr = item.CoreType?.ToString() ?? ""; + var toPath = Utils.GetBinPath("", coreTypeStr); if (fileName.Contains(".tar.gz")) { @@ -284,7 +369,7 @@ private async Task UpgradeCore() } else if (fileName.Contains(".gz")) { - FileUtils.DecompressFile(fileName, toPath, item.CoreType); + FileUtils.DecompressFile(fileName, toPath, coreTypeStr); } else { @@ -296,7 +381,7 @@ private async Task UpgradeCore() var filesList = new DirectoryInfo(toPath).GetFiles().Select(u => u.FullName).ToList(); foreach (var file in filesList) { - await Utils.SetLinuxChmod(Path.Combine(toPath, item.CoreType.ToLower())); + await Utils.SetLinuxChmod(Path.Combine(toPath, coreTypeStr.ToLower())); } } @@ -309,11 +394,12 @@ private async Task UpgradeCore() } } - private async Task UpdateView(string coreType, string msg) + private async Task UpdateView(ECoreType? coreType, string msg) { var item = new CheckUpdateModel() { CoreType = coreType, + IsGeoFile = coreType == null, Remarks = msg, }; @@ -327,7 +413,7 @@ private async Task UpdateView(string coreType, string msg) public async Task UpdateViewResult(CheckUpdateModel model) { - var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType); + var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType && t.IsGeoFile == model.IsGeoFile); if (found == null) { return; diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index 6220719b39d..7da8ad42321 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -1,6 +1,6 @@ using System.Reactive.Concurrency; -using static ServiceLib.Models.ClashProviders; -using static ServiceLib.Models.ClashProxies; +using static ServiceLib.Models.Dto.ClashProviders; +using static ServiceLib.Models.Dto.ClashProxies; namespace ServiceLib.ViewModels; diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 53ad06c8068..4b3f41b9393 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -65,6 +65,8 @@ public class MainWindowViewModel : MyReactiveObject [Reactive] public bool BlIsWindows { get; set; } + [Reactive] public bool BlNewUpdate { get; set; } + #endregion Menu #region Init @@ -251,6 +253,11 @@ public MainWindowViewModel(Func>? updateView) .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy)); + AppEvents.HasUpdateNotified + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async bl => BlNewUpdate = bl); + #endregion AppEvents _ = Init(); @@ -297,10 +304,22 @@ private async Task UpdateTaskHandler(bool success, string msg) { var indexIdOld = _config.IndexId; await RefreshServers(); - if (indexIdOld != _config.IndexId) + + // If indexId changed or subIndexId is empty, directly reload. + if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty()) { await Reload(); } + else + { + // The activity config belongs to the current group. + var profile = await AppManager.Instance.GetProfileItem(_config.IndexId); + if (profile != null && profile.Subid == _config.SubIndexId) + { + await Reload(); + } + } + if (_config.UiItem.EnableAutoAdjustMainLvColWidth) { AppEvents.AdjustMainLvColWidthRequested.Publish(); diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 122772c4cfc..583502c8686 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -4,29 +4,29 @@ public class OptionSettingViewModel : MyReactiveObject { #region Core - [Reactive] public int localPort { get; set; } + [Reactive] public int LocalPort { get; set; } [Reactive] public bool SecondLocalPortEnabled { get; set; } - [Reactive] public bool udpEnabled { get; set; } - [Reactive] public bool sniffingEnabled { get; set; } - public IList destOverride { get; set; } - [Reactive] public bool routeOnly { get; set; } - [Reactive] public bool allowLANConn { get; set; } - [Reactive] public bool newPort4LAN { get; set; } - [Reactive] public string user { get; set; } - [Reactive] public string pass { get; set; } - [Reactive] public bool muxEnabled { get; set; } - [Reactive] public bool logEnabled { get; set; } - [Reactive] public string loglevel { get; set; } - [Reactive] public bool defAllowInsecure { get; set; } - [Reactive] public string defFingerprint { get; set; } - [Reactive] public string defUserAgent { get; set; } - [Reactive] public string sendThrough { get; set; } - [Reactive] public string bindInterface { 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 bool enableFragment { get; set; } + [Reactive] public bool UdpEnabled { get; set; } + [Reactive] public bool SniffingEnabled { get; set; } + public IList DestOverride { get; set; } + [Reactive] public bool RouteOnly { get; set; } + [Reactive] public bool AllowLANConn { get; set; } + [Reactive] public bool NewPort4LAN { get; set; } + [Reactive] public string User { get; set; } + [Reactive] public string Pass { get; set; } + [Reactive] public bool MuxEnabled { get; set; } + [Reactive] public bool LogEnabled { get; set; } + [Reactive] public string Loglevel { get; set; } + [Reactive] public bool DefAllowInsecure { get; set; } + [Reactive] public string DefFingerprint { get; set; } + [Reactive] public string DefUserAgent { get; set; } + [Reactive] public string SendThrough { get; set; } + [Reactive] public string BindInterface { 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 bool EnableFragment { get; set; } #endregion Core @@ -83,9 +83,9 @@ public class OptionSettingViewModel : MyReactiveObject #region System proxy - [Reactive] public bool notProxyLocalAddress { get; set; } - [Reactive] public string systemProxyAdvancedProtocol { get; set; } - [Reactive] public string systemProxyExceptions { get; set; } + [Reactive] public bool NotProxyLocalAddress { get; set; } + [Reactive] public string SystemProxyAdvancedProtocol { get; set; } + [Reactive] public string SystemProxyExceptions { get; set; } [Reactive] public string CustomSystemProxyPacPath { get; set; } [Reactive] public string CustomSystemProxyScriptPath { get; set; } @@ -100,6 +100,7 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public bool TunEnableIPv6Address { get; set; } [Reactive] public string TunIcmpRouting { get; set; } [Reactive] public bool TunEnableLegacyProtect { get; set; } + [Reactive] public string TunRouteExcludeAddress { get; set; } #endregion Tun mode @@ -142,28 +143,28 @@ private async Task Init() #region Core var inbound = _config.Inbound.First(); - localPort = inbound.LocalPort; + LocalPort = inbound.LocalPort; SecondLocalPortEnabled = inbound.SecondLocalPortEnabled; - udpEnabled = inbound.UdpEnabled; - sniffingEnabled = inbound.SniffingEnabled; - routeOnly = inbound.RouteOnly; - allowLANConn = inbound.AllowLANConn; - newPort4LAN = inbound.NewPort4LAN; - user = inbound.User; - pass = inbound.Pass; - muxEnabled = _config.CoreBasicItem.MuxEnabled; - logEnabled = _config.CoreBasicItem.LogEnabled; - loglevel = _config.CoreBasicItem.Loglevel; - defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure; - defFingerprint = _config.CoreBasicItem.DefFingerprint; - defUserAgent = _config.CoreBasicItem.DefUserAgent; - sendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty; - bindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty; - mux4SboxProtocol = _config.Mux4SboxItem.Protocol; - enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox; - hyUpMbps = _config.HysteriaItem.UpMbps; - hyDownMbps = _config.HysteriaItem.DownMbps; - enableFragment = _config.CoreBasicItem.EnableFragment; + UdpEnabled = inbound.UdpEnabled; + SniffingEnabled = inbound.SniffingEnabled; + RouteOnly = inbound.RouteOnly; + AllowLANConn = inbound.AllowLANConn; + NewPort4LAN = inbound.NewPort4LAN; + User = inbound.User; + Pass = inbound.Pass; + MuxEnabled = _config.CoreBasicItem.MuxEnabled; + LogEnabled = _config.CoreBasicItem.LogEnabled; + Loglevel = _config.CoreBasicItem.Loglevel; + DefAllowInsecure = _config.CoreBasicItem.DefAllowInsecure; + DefFingerprint = _config.CoreBasicItem.DefFingerprint; + DefUserAgent = _config.CoreBasicItem.DefUserAgent; + SendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty; + BindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty; + Mux4SboxProtocol = _config.Mux4SboxItem.Protocol; + EnableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox; + HyUpMbps = _config.HysteriaItem.UpMbps; + HyDownMbps = _config.HysteriaItem.DownMbps; + EnableFragment = _config.CoreBasicItem.EnableFragment; #endregion Core @@ -211,9 +212,9 @@ private async Task Init() #region System proxy - notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; - systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; - systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; + NotProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; + SystemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; + SystemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath; CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath; @@ -228,6 +229,7 @@ private async Task Init() TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address; TunIcmpRouting = _config.TunModeItem.IcmpRouting; TunEnableLegacyProtect = _config.TunModeItem.EnableLegacyProtect; + TunRouteExcludeAddress = Utils.List2String(_config.TunModeItem.RouteExcludeAddress, true); #endregion Tun mode @@ -297,13 +299,13 @@ private async Task InitCoreType() private async Task SaveSettingAsync() { - if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString()) - || localPort <= 0 || localPort >= Global.MaxPort) + if (LocalPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(LocalPort.ToString()) + || LocalPort <= 0 || LocalPort >= Global.MaxPort) { NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); return; } - var sendThroughValue = sendThrough.TrimEx(); + var sendThroughValue = SendThrough.TrimEx(); if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue)) { NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4); @@ -328,33 +330,33 @@ private async Task SaveSettingAsync() //} //Core - _config.Inbound.First().LocalPort = localPort; + _config.Inbound.First().LocalPort = LocalPort; _config.Inbound.First().SecondLocalPortEnabled = SecondLocalPortEnabled; - _config.Inbound.First().UdpEnabled = udpEnabled; - _config.Inbound.First().SniffingEnabled = sniffingEnabled; - _config.Inbound.First().DestOverride = destOverride?.ToList(); - _config.Inbound.First().RouteOnly = routeOnly; - _config.Inbound.First().AllowLANConn = allowLANConn; - _config.Inbound.First().NewPort4LAN = newPort4LAN; - _config.Inbound.First().User = user; - _config.Inbound.First().Pass = pass; + _config.Inbound.First().UdpEnabled = UdpEnabled; + _config.Inbound.First().SniffingEnabled = SniffingEnabled; + _config.Inbound.First().DestOverride = DestOverride?.ToList(); + _config.Inbound.First().RouteOnly = RouteOnly; + _config.Inbound.First().AllowLANConn = AllowLANConn; + _config.Inbound.First().NewPort4LAN = NewPort4LAN; + _config.Inbound.First().User = User; + _config.Inbound.First().Pass = Pass; if (_config.Inbound.Count > 1) { _config.Inbound.RemoveAt(1); } - _config.CoreBasicItem.LogEnabled = logEnabled; - _config.CoreBasicItem.Loglevel = loglevel; - _config.CoreBasicItem.MuxEnabled = muxEnabled; - _config.CoreBasicItem.DefAllowInsecure = defAllowInsecure; - _config.CoreBasicItem.DefFingerprint = defFingerprint; - _config.CoreBasicItem.DefUserAgent = defUserAgent; - _config.CoreBasicItem.SendThrough = sendThrough.TrimEx(); - _config.CoreBasicItem.BindInterface = bindInterface.TrimEx(); - _config.Mux4SboxItem.Protocol = mux4SboxProtocol; - _config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox; - _config.HysteriaItem.UpMbps = hyUpMbps ?? 0; - _config.HysteriaItem.DownMbps = hyDownMbps ?? 0; - _config.CoreBasicItem.EnableFragment = enableFragment; + _config.CoreBasicItem.LogEnabled = LogEnabled; + _config.CoreBasicItem.Loglevel = Loglevel; + _config.CoreBasicItem.MuxEnabled = MuxEnabled; + _config.CoreBasicItem.DefAllowInsecure = DefAllowInsecure; + _config.CoreBasicItem.DefFingerprint = DefFingerprint; + _config.CoreBasicItem.DefUserAgent = DefUserAgent; + _config.CoreBasicItem.SendThrough = SendThrough.TrimEx(); + _config.CoreBasicItem.BindInterface = BindInterface.TrimEx(); + _config.Mux4SboxItem.Protocol = Mux4SboxProtocol; + _config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox; + _config.HysteriaItem.UpMbps = HyUpMbps ?? 0; + _config.HysteriaItem.DownMbps = HyDownMbps ?? 0; + _config.CoreBasicItem.EnableFragment = EnableFragment; _config.GuiItem.AutoRun = AutoRun; _config.GuiItem.EnableStatistics = EnableStatistics; @@ -383,9 +385,9 @@ private async Task SaveSettingAsync() _config.SpeedTestItem.IPAPIUrl = IPAPIUrl; //systemProxy - _config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions; - _config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress; - _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; + _config.SystemProxyItem.SystemProxyExceptions = SystemProxyExceptions; + _config.SystemProxyItem.NotProxyLocalAddress = NotProxyLocalAddress; + _config.SystemProxyItem.SystemProxyAdvancedProtocol = SystemProxyAdvancedProtocol; _config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath; _config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath; @@ -397,6 +399,7 @@ private async Task SaveSettingAsync() _config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address; _config.TunModeItem.IcmpRouting = TunIcmpRouting; _config.TunModeItem.EnableLegacyProtect = TunEnableLegacyProtect; + _config.TunModeItem.RouteExcludeAddress = Utils.String2List(TunRouteExcludeAddress); //coreType await SaveCoreType(); diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 609ae0d16c9..1af0ac14724 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -303,6 +303,10 @@ public async Task SetSpeedTestResult(SpeedTestResult result) { item.SpeedVal = result.Speed ?? string.Empty; } + if (result.IpInfo.IsNotEmpty()) + { + item.IpInfo = result.IpInfo ?? string.Empty; + } await Task.CompletedTask; } @@ -437,6 +441,7 @@ from t33 in t3b.DefaultIfEmpty() Speed = t33?.Speed ?? 0, DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty, SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty, + IpInfo = t33?.IpInfo ?? string.Empty, TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown), TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp), TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown), diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs index 35371f0f193..c1c448cb78e 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml.cs +++ b/v2rayN/v2rayN.Desktop/App.axaml.cs @@ -1,3 +1,4 @@ +using v2rayN.Desktop.Common; using v2rayN.Desktop.Views; namespace v2rayN.Desktop; @@ -24,11 +25,34 @@ public override void OnFrameworkInitializationCompleted() desktop.Exit += OnExit; desktop.MainWindow = new MainWindow(); + + if (OperatingSystem.IsMacOS()) + { + Current?.TryGetFeature()?.Activated += OnMacOSActivated; + } } base.OnFrameworkInitializationCompleted(); } + private void OnMacOSActivated(object? sender, ActivatedEventArgs args) + { + if (args.Kind != ActivationKind.Reopen) + { + return; + } + + Dispatcher.UIThread.Post(() => + { + ((ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow)?.ShowHideWindow(true); + + if (!AppManager.Instance.Config.UiItem.MacOSShowInDock) + { + MacAppUtils.SetActivationPolicyAccessory(); + } + }); + } + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject != null) diff --git a/v2rayN/v2rayN.Desktop/Common/MacAppUtils.cs b/v2rayN/v2rayN.Desktop/Common/MacAppUtils.cs new file mode 100644 index 00000000000..b38f75072d7 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/MacAppUtils.cs @@ -0,0 +1,27 @@ +using System.Runtime.InteropServices; + +namespace v2rayN.Desktop.Common; + +internal static class MacAppUtils +{ + private const string LibObjC = "/usr/lib/libobjc.dylib"; + private const nint ActivationPolicyAccessory = 1; + + public static void SetActivationPolicyAccessory() + => objc_msgSend( + objc_msgSend(objc_getClass("NSApplication"), sel_registerName("sharedApplication")), + sel_registerName("setActivationPolicy:"), + ActivationPolicyAccessory); + + [DllImport(LibObjC)] + private static extern nint objc_getClass(string name); + + [DllImport(LibObjC)] + private static extern nint sel_registerName(string name); + + [DllImport(LibObjC, EntryPoint = "objc_msgSend")] + private static extern nint objc_msgSend(nint receiver, nint selector); + + [DllImport(LibObjC, EntryPoint = "objc_msgSend")] + private static extern void objc_msgSend(nint receiver, nint selector, nint argument); +} diff --git a/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs b/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs index 90f60c7f273..ba416e719f1 100644 --- a/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs +++ b/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs @@ -23,6 +23,7 @@ public partial class QRCodeAvaloniaUtils } } + [SupportedOSPlatform("windows")] private static byte[]? CaptureScreenWindows() { var hdcScreen = IntPtr.Zero; diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs index 5b0b215876d..558852d255d 100644 --- a/v2rayN/v2rayN.Desktop/GlobalUsings.cs +++ b/v2rayN/v2rayN.Desktop/GlobalUsings.cs @@ -5,6 +5,7 @@ global using System.Linq; global using System.Reactive.Disposables.Fluent; global using System.Reactive.Linq; +global using System.Runtime.Versioning; global using System.Text; global using System.Threading; global using System.Threading.Tasks; @@ -30,6 +31,9 @@ global using ServiceLib.Events; global using ServiceLib.Handler; global using ServiceLib.Manager; -global using ServiceLib.Models; +global using ServiceLib.Models.CoreConfigs; +global using ServiceLib.Models.Configs; +global using ServiceLib.Models.Dto; +global using ServiceLib.Models.Entities; global using ServiceLib.Resx; global using ServiceLib.ViewModels; diff --git a/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs index f803c538ded..a79573d6f60 100644 --- a/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs +++ b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs @@ -8,6 +8,7 @@ public sealed class HotkeyManager private static readonly Lazy _instance = new(() => new()); public static HotkeyManager Instance = _instance.Value; private readonly Dictionary _hotkeyTriggerDic = new(); + [SupportedOSPlatform("windows")] private GlobalHotKeys.HotKeyManager? _hotKeyManager; private Config? _config; @@ -16,6 +17,7 @@ public sealed class HotkeyManager public bool IsPause { get; set; } = false; + [SupportedOSPlatform("windows")] public void Init(Config config, Action updateFunc) { _config = config; @@ -26,9 +28,14 @@ public void Init(Config config, Action updateFunc) public void Dispose() { + if (!Utils.IsWindows()) + { + return; + } _hotKeyManager?.Dispose(); } + [SupportedOSPlatform("windows")] private void Register() { if (_config.GlobalHotkeys.Any(t => t.KeyCode > 0) == false) diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index dbf031b11a6..574d4716862 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -1060,7 +1060,7 @@ Grid.Row="8" ColumnDefinitions="300,Auto" IsVisible="False" - RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + + + vm.AllowInsecureCertFetch, v => v.togAllowInsecureCertFetch.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.txtAllowInsecureCertFetchTips.IsVisible).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); //reality this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); diff --git a/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml b/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml index 0055f9adc05..5d103373170 100644 --- a/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/CheckUpdateView.axaml @@ -33,6 +33,12 @@ Margin="{StaticResource Margin4}" HorizontalAlignment="Left" /> +