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-all.yml b/.github/workflows/build-all.yml index 03b7d834bb7..c7d486bf89b 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -7,13 +7,16 @@ on: required: false type: string +permissions: + actions: write + jobs: update: runs-on: ubuntu-latest steps: - name: Trigger build windows - if: github.event.inputs.release_tag != '' + if: inputs.release_tag != '' run: | curl -X POST \ -H "Accept: application/vnd.github.v3+json" \ @@ -22,12 +25,12 @@ jobs: -d "{ \"ref\": \"master\", \"inputs\": { - \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + \"release_tag\": \"${{ inputs.release_tag }}\" } }" - name: Trigger build linux - if: github.event.inputs.release_tag != '' + if: inputs.release_tag != '' run: | curl -X POST \ -H "Accept: application/vnd.github.v3+json" \ @@ -36,12 +39,12 @@ jobs: -d "{ \"ref\": \"master\", \"inputs\": { - \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + \"release_tag\": \"${{ inputs.release_tag }}\" } }" - name: Trigger build osx - if: github.event.inputs.release_tag != '' + if: inputs.release_tag != '' run: | curl -X POST \ -H "Accept: application/vnd.github.v3+json" \ @@ -50,12 +53,12 @@ jobs: -d "{ \"ref\": \"master\", \"inputs\": { - \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + \"release_tag\": \"${{ inputs.release_tag }}\" } }" - name: Trigger build windows desktop - if: github.event.inputs.release_tag != '' + if: inputs.release_tag != '' run: | curl -X POST \ -H "Accept: application/vnd.github.v3+json" \ @@ -64,6 +67,6 @@ jobs: -d "{ \"ref\": \"master\", \"inputs\": { - \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + \"release_tag\": \"${{ inputs.release_tag }}\" } - }" \ No newline at end of file + }" diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3f2fe03342d..baeae29a35d 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -13,73 +13,29 @@ on: permissions: contents: write -env: - OutputArch: "linux-64" - OutputArchArm: "linux-arm64" - OutputPath64: "${{ github.workspace }}/v2rayN/Release/linux-64" - OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/linux-arm64" - jobs: build: - strategy: - matrix: - configuration: [Release] - runs-on: ubuntu-24.04 - - steps: - - name: Checkout - uses: actions/checkout@v6.0.2 - with: - submodules: 'recursive' - fetch-depth: '0' - - - name: Setup .NET - uses: actions/setup-dotnet@v5.2.0 - with: - dotnet-version: '8.0.x' + uses: ./.github/workflows/build.yml + with: + target: linux - - name: Build - run: | - cd v2rayN - dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-x64 -p:SelfContained=true -o "$OutputPath64" - dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-arm64 -p:SelfContained=true -o "$OutputPathArm64" - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-x64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPath64" - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPathArm64" - - - name: Upload build artifacts - uses: actions/upload-artifact@v7.0.0 - with: - name: v2rayN-linux - path: | - ${{ github.workspace }}/v2rayN/Release/linux* - - # release zip archive - - name: Package release zip archive - if: github.event.inputs.release_tag != '' - run: | - chmod 755 package-release-zip.sh - ./package-release-zip.sh "$OutputArch" "$OutputPath64" - ./package-release-zip.sh "$OutputArchArm" "$OutputPathArm64" - - - name: Upload zip archive to release - uses: svenstaro/upload-release-action@v2 - if: github.event.inputs.release_tag != '' - with: - file: ${{ github.workspace }}/v2rayN*.zip - tag: ${{ github.event.inputs.release_tag }} - file_glob: true - prerelease: true + release-zip: + if: inputs.release_tag != '' + needs: build + uses: ./.github/workflows/package-zip.yml + with: + target: linux + release_tag: ${{ inputs.release_tag }} deb: - needs: build + name: build and release deb x64 & arm64 if: | - (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') || + (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) runs-on: ubuntu-24.04 - container: - image: debian:13 + container: debian:13 env: - RELEASE_TAG: ${{ github.event.inputs.release_tag != '' && github.event.inputs.release_tag || github.ref_name }} + RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} steps: - name: Prepare tools (Debian) @@ -93,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' @@ -115,12 +71,6 @@ jobs: echo "==== Dist tree ====" ls -R "$GITHUB_WORKSPACE/dist/deb" || true - - name: Upload DEB artifacts - uses: actions/upload-artifact@v7.0.0 - with: - name: v2rayN-deb - path: dist/deb/**/*.deb - - name: Upload DEBs to release uses: svenstaro/upload-release-action@v2 with: @@ -130,15 +80,14 @@ jobs: prerelease: true rpm: - needs: build + name: build and release rpm x64 & arm64 if: | - (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') || + (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) runs-on: ubuntu-24.04 - container: - image: registry.access.redhat.com/ubi10/ubi + container: registry.access.redhat.com/ubi10/ubi env: - RELEASE_TAG: ${{ github.event.inputs.release_tag != '' && github.event.inputs.release_tag || github.ref_name }} + RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} steps: - name: Prepare tools (Red Hat) @@ -202,17 +151,11 @@ 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' - - name: Restore build artifacts - uses: actions/download-artifact@v8 - with: - name: v2rayN-linux - path: ${{ github.workspace }}/v2rayN/Release - - name: Ensure script permissions run: chmod 755 package-rhel.sh @@ -228,16 +171,332 @@ jobs: echo "==== Dist tree ====" ls -R "$GITHUB_WORKSPACE/dist/rpm" || true - - name: Upload RPM artifacts - uses: actions/upload-artifact@v7.0.0 + - name: Upload RPMs to release + uses: svenstaro/upload-release-action@v2 with: - name: v2rayN-rpm - path: dist/rpm/**/*.rpm + file: dist/rpm/**/*.rpm + tag: ${{ env.RELEASE_TAG }} + file_glob: true + prerelease: true + + rpm-riscv64: + name: build and release rpm 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: rockylinux/rockylinux:10 + env: + RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} + + steps: + - name: Prepare tools (Red Hat) + shell: bash + run: | + set -euo pipefail + dnf -y makecache + dnf -y install \ + sudo git rpm-build rpmdevtools dnf-plugins-core \ + rsync findutils tar gzip unzip which jq + + - 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-rhel-riscv.sh + + - name: Package RPM (RHEL-family) + run: ./package-rhel-riscv.sh "${RELEASE_TAG}" + + - name: Collect RPMs into workspace + run: | + mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64" + rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true + find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" \ + -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true + echo "==== Dist tree ====" + ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true - name: Upload RPMs to release + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + shopt -s globstar nullglob + + files=(dist/rpm-riscv64/**/*.rpm) + (( ${#files[@]} )) || { echo "No RPMs 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/x-rpm" \ + --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/rpm/**/*.rpm + 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 d79295cc599..340fc32f701 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -10,78 +10,65 @@ on: branches: - master -env: - OutputArch: "macos-64" - OutputArchArm: "macos-arm64" - OutputPath64: "${{ github.workspace }}/v2rayN/Release/macos-64" - OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/macos-arm64" +permissions: + contents: write jobs: build: + uses: ./.github/workflows/build.yml + with: + target: macos + + release-zip: + if: inputs.release_tag != '' + needs: build + uses: ./.github/workflows/package-zip.yml + with: + target: macos + release_tag: ${{ inputs.release_tag }} + + dmg: + name: package and release macOS dmg + if: inputs.release_tag != '' + needs: build strategy: matrix: - configuration: [Release] - + arch: [ x64, arm64 ] runs-on: macos-latest - + env: + Arch: |- + ${{ + case( + matrix.arch == 'x64', '64', + matrix.arch + ) + }} steps: - name: Checkout - uses: actions/checkout@v6.0.2 - with: - submodules: 'recursive' - fetch-depth: '0' + uses: actions/checkout@v6 - - name: Setup - uses: actions/setup-dotnet@v5.2.0 + - name: Restore build artifacts + uses: actions/download-artifact@v8 with: - dotnet-version: '8.0.x' + name: ${{ matrix.arch }} + path: v2rayN-macos-${{ env.Arch }} + + - name: Setup create-dmg + run: brew install create-dmg - - name: Build - run: | - cd v2rayN - dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r osx-x64 -p:SelfContained=true -o $OutputPath64 - dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r osx-arm64 -p:SelfContained=true -o $OutputPathArm64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-x64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPath64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPathArm64 + - name: Ensure script permissions + run: chmod 755 package-osx.sh - - name: Upload build artifacts - uses: actions/upload-artifact@v7.0.0 - with: - name: v2rayN-macos - path: | - ${{ github.workspace }}/v2rayN/Release/macos* - - # release osx package - - name: Package osx - if: github.event.inputs.release_tag != '' - run: | - brew install create-dmg - chmod 755 package-osx.sh - ./package-osx.sh $OutputArch $OutputPath64 ${{ github.event.inputs.release_tag }} - ./package-osx.sh $OutputArchArm $OutputPathArm64 ${{ github.event.inputs.release_tag }} + - name: Package dmg + run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }} + + - name: Sleep for race condition between matrix jobs + 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 - if: github.event.inputs.release_tag != '' with: file: ${{ github.workspace }}/v2rayN*.dmg - tag: ${{ github.event.inputs.release_tag }} + tag: ${{ inputs.release_tag }} file_glob: true prerelease: true - - # release zip archive - - name: Package release zip archive - if: github.event.inputs.release_tag != '' - run: | - chmod 755 package-release-zip.sh - ./package-release-zip.sh $OutputArch $OutputPath64 - ./package-release-zip.sh $OutputArchArm $OutputPathArm64 - - - name: Upload zip archive to release - uses: svenstaro/upload-release-action@v2 - if: github.event.inputs.release_tag != '' - with: - file: ${{ github.workspace }}/v2rayN*.zip - tag: ${{ github.event.inputs.release_tag }} - file_glob: true - prerelease: true \ No newline at end of file diff --git a/.github/workflows/build-windows-desktop.yml b/.github/workflows/build-windows-desktop.yml index 99c7ef328be..9a9579a4787 100644 --- a/.github/workflows/build-windows-desktop.yml +++ b/.github/workflows/build-windows-desktop.yml @@ -10,62 +10,19 @@ on: branches: - master -env: - OutputArch: "windows-64" - OutputArchArm: "windows-arm64" - OutputPath64: "${{ github.workspace }}/v2rayN/Release/windows-64" - OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/windows-arm64" +permissions: + contents: write jobs: build: - strategy: - matrix: - configuration: [Release] - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6.0.2 - with: - submodules: 'recursive' - fetch-depth: '0' - - - name: Setup - uses: actions/setup-dotnet@v5.2.0 - with: - dotnet-version: '8.0.x' - - - name: Build - run: | - cd v2rayN - dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPath64 - dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPathArm64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64 - - - name: Upload build artifacts - uses: actions/upload-artifact@v7.0.0 - with: - name: v2rayN-windows-desktop - path: | - ${{ github.workspace }}/v2rayN/Release/windows* - - # release zip archive - - name: Package release zip archive - if: github.event.inputs.release_tag != '' - run: | - chmod 755 package-release-zip.sh - ./package-release-zip.sh $OutputArch $OutputPath64 - mv "v2rayN-${OutputArch}.zip" "v2rayN-${OutputArch}-desktop.zip" - ./package-release-zip.sh $OutputArchArm $OutputPathArm64 - mv "v2rayN-${OutputArchArm}.zip" "v2rayN-${OutputArchArm}-desktop.zip" - - - name: Upload zip archive to release - uses: svenstaro/upload-release-action@v2 - if: github.event.inputs.release_tag != '' - with: - file: ${{ github.workspace }}/v2rayN*.zip - tag: ${{ github.event.inputs.release_tag }} - file_glob: true - prerelease: true + uses: ./.github/workflows/build.yml + with: + target: windows + + release-zip: + if: inputs.release_tag != '' + needs: build + uses: ./.github/workflows/package-zip.yml + with: + target: windows-desktop + release_tag: ${{ inputs.release_tag }} diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 7d202370b9e..4bcba3d6908 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -10,57 +10,20 @@ on: branches: - master -env: - OutputArch: "windows-64" - OutputArchArm: "windows-arm64" - OutputPath64: "${{ github.workspace }}/v2rayN/Release/windows-64" - OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/windows-arm64" +permissions: + contents: write jobs: build: - strategy: - matrix: - configuration: [Release] + uses: ./.github/workflows/build.yml + with: + target: windows + project: ./v2rayN/v2rayN.csproj - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6.0.2 - - - name: Setup - uses: actions/setup-dotnet@v5.2.0 - with: - dotnet-version: '8.0.x' - - - name: Build - run: | - cd v2rayN - dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPath64 - dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPathArm64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64 - - - name: Upload build artifacts - uses: actions/upload-artifact@v7.0.0 - with: - name: v2rayN-windows - path: | - ${{ github.workspace }}/v2rayN/Release/windows* - - # release zip archive - - name: Package release zip archive - if: github.event.inputs.release_tag != '' - run: | - chmod 755 package-release-zip.sh - ./package-release-zip.sh $OutputArch $OutputPath64 - ./package-release-zip.sh $OutputArchArm $OutputPathArm64 - - - name: Upload zip archive to release - uses: svenstaro/upload-release-action@v2 - if: github.event.inputs.release_tag != '' - with: - file: ${{ github.workspace }}/v2rayN*.zip - tag: ${{ github.event.inputs.release_tag }} - file_glob: true - prerelease: true + release-zip: + if: inputs.release_tag != '' + needs: build + uses: ./.github/workflows/package-zip.yml + with: + target: windows + release_tag: ${{ inputs.release_tag }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..8c73a6c09e5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,71 @@ +name: build + +on: + workflow_call: + inputs: + target: # windows linux macos + required: true + type: string + project: + required: false + type: string + default: './v2rayN.Desktop/v2rayN.Desktop.csproj' + +jobs: + build: + name: build x64 arm64 + strategy: + matrix: + arch: [ x64, arm64 ] + runs-on: |- + ${{ + case( + inputs.target == 'macos', 'macos-latest', + inputs.target == 'linux', 'ubuntu-24.04', + 'ubuntu-latest' + ) + }} + env: + Output: "${{ github.workspace }}/${{ matrix.arch }}" + RID: |- + ${{ + case( + inputs.target == 'macos', format('osx-{0}', matrix.arch), + inputs.target == 'windows', format('win-{0}', matrix.arch), + format('{0}-{1}', inputs.target, matrix.arch) + ) + }} + Project: ${{ inputs.project }} + ExtOpt: |- + ${{ + case( + inputs.target == 'windows', '-p:EnableWindowsTargeting=true', + '' + ) + }} + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Setup .NET + uses: actions/setup-dotnet@v5.3.0 + with: + dotnet-version: '10.0.1xx' + + - name: Build v2rayN + working-directory: ./v2rayN + run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output + + - name: Build AmazTool + working-directory: ./v2rayN + run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output + + - name: Upload build artifacts + uses: actions/upload-artifact@v7.0.1 + with: + name: ${{ 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 new file mode 100644 index 00000000000..1f9b3613996 --- /dev/null +++ b/.github/workflows/package-zip.yml @@ -0,0 +1,67 @@ +name: package and release Zip + +on: + workflow_call: + inputs: + release_tag: + required: true + type: string + target: # windows linux macos windows-desktop + required: true + type: string + +permissions: + contents: write + +jobs: + package: + name: package x64 arm64 + strategy: + matrix: + arch: [ x64, arm64 ] + runs-on: ubuntu-latest + env: + Target: |- + ${{ + case( + inputs.target == 'windows-desktop', 'windows', + inputs.target + ) + }} + Arch: |- + ${{ + case( + matrix.arch == 'x64', '64', + matrix.arch + ) + }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Restore build artifacts + uses: actions/download-artifact@v8 + with: + name: ${{ matrix.arch }} + path: v2rayN-${{ env.Target }}-${{ env.Arch }} + + - name: Get v2rayN-core-bin + run: wget -nv -O v2rayN-$Target-$Arch.zip "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-$Target-$Arch.zip" + + - name: Package zip archive + run: 7z a -tZip v2rayN-$Target-$Arch.zip v2rayN-$Target-$Arch -mx1 + + - name: Rename windows-desktop + if: inputs.target == 'windows-desktop' + run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip" + + - name: Sleep for race condition between matrix jobs + 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: + file: ${{ github.workspace }}/v2rayN*.zip + tag: ${{ inputs.release_tag }} + file_glob: true + prerelease: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000000..633bb8e2e97 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Code Test + +on: + pull_request: + branches: + - master + paths: + - 'v2rayN/ServiceLib/Services/CoreConfig/**' + - 'v2rayN/ServiceLib/Handler/Fmt/**' + - '.github/workflows/test.yml' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Setup .NET + uses: actions/setup-dotnet@v5.3.0 + with: + dotnet-version: '8.0.x' + + - name: Test Code + working-directory: ./v2rayN + run: dotnet test ./ServiceLib.Tests 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 fa21461df8e..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,94 +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() { - local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin + 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 + | 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" || 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" @@ -296,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" @@ -353,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" @@ -361,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" @@ -464,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" - mkdir -p "$WORKDIR/debian" - cat > "$WORKDIR/debian/control" </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" < "$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 - - # 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 + 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 - # 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-release-zip.sh b/package-release-zip.sh deleted file mode 100644 index 60804e6928d..00000000000 --- a/package-release-zip.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -Arch="$1" -OutputPath="$2" - -OutputArch="v2rayN-${Arch}" -FileName="v2rayN-${Arch}.zip" - -wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/$FileName" - -ZipPath64="./$OutputArch" -mkdir $ZipPath64 - -cp -rf $OutputPath "$ZipPath64/$OutputArch" -7z a -tZip $FileName "$ZipPath64/$OutputArch" -mx1 \ No newline at end of file diff --git a/package-rhel-riscv.sh b/package-rhel-riscv.sh new file mode 100644 index 00000000000..9c4fa03fb27 --- /dev/null +++ b/package-rhel-riscv.sh @@ -0,0 +1,695 @@ +#!/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" +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}" + +OS_ID="" +OS_NAME="" +OS_VERSION_ID="" +HOST_ARCH="" +SCRIPT_DIR="" +PROJECT="" +VERSION="" + +declare -a BUILT_RPMS=() + +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 + 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 + + 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="" + + 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" + + export PATH="$HOME/.dotnet:$PATH" + export DOTNET_ROOT="$HOME/.dotnet" + + dotnet --info >/dev/null 2>&1 || install_ok=0 + 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 +} + +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%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 +} + +write_spec_file() { + local specfile="$1" + + cat > "$specfile" <<'SPEC' +%global debug_package %{nil} +%undefine _debuginfo_subpackages +%undefine _debugsource_packages +%global __requires_exclude ^liblttng-ust\.so\..*$ + +Name: v2rayN +Version: __VERSION__ +Release: 1%{?dist} +Summary: v2rayN (Avalonia) GUI client for Linux (riscv64) +License: GPL-3.0-only +URL: https://github.com/2dust/v2rayN +BugURL: https://github.com/2dust/v2rayN/issues +ExclusiveArch: riscv64 +Source0: __PKGROOT__.tar.gz + +Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL +Requires: glibc >= 2.34 +Requires: fontconfig >= 2.13.1 +Requires: desktop-file-utils >= 0.26 +Requires: xdg-utils >= 1.1.3 +Requires: coreutils >= 8.32 +Requires: bash >= 5.1 +Requires: freetype >= 2.10 + +%description +v2rayN Linux for Red Hat Enterprise Linux +Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard +Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS +For more information, Please visit our website +https://github.com/2dust/v2rayN + +%prep +%setup -q -n __PKGROOT__ + +%build + +%install +install -dm0755 %{buildroot}/opt/v2rayN +cp -a * %{buildroot}/opt/v2rayN/ + +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 || : + +install -dm0755 %{buildroot}%{_bindir} +install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF' +#!/usr/bin/bash +set -euo pipefail +DIR="/opt/v2rayN" + +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 + +install -dm0755 %{buildroot}%{_datadir}/applications +install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF' +[Desktop Entry] +Type=Application +Name=v2rayN +Comment=v2rayN for Red Hat Enterprise Linux +Exec=v2rayn +Icon=v2rayn +Terminal=false +Categories=Network; +EOF + +install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps +install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png + +%post +/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true +/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true + +%postun +/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true +/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true + +%files +%{_bindir}/v2rayn +/opt/v2rayN +%{_datadir}/applications/v2rayn.desktop +%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png +SPEC + + 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" + + 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:" + for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do + [[ -e "$f" ]] || continue + echo " $f" + BUILT_RPMS+=("$f") + done +} + +select_targets() { + printf '%s\n' riscv64 +} + +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" + 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 + + print_summary +} + +main "$@" diff --git a/package-rhel.sh b/package-rhel.sh index bf295134349..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)" -install_ok=0 + [[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL" + echo "[OK] Kernel $current_kernel verified." +} -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 +install_dependencies() { + local install_ok=0 -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 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 -# Root directory -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" + 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 +} -# Git submodules (best effort) -if [[ -f .gitmodules ]]; then - git submodule sync --recursive || true - git submodule update --init --recursive || true -fi +prepare_workspace() { + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + cd "$SCRIPT_DIR" -# 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; } + 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() { - # 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,98 +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 + 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 + | 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" || 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" @@ -272,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" @@ -335,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" @@ -343,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" -# ===== 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 + mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box" - echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray 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 + 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 - - # 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)" - } + 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 @@ -458,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 @@ -479,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 @@ -510,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] @@ -523,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 @@ -542,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" +} + +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" - # Build RPM for this arch - rpmbuild -ba "$SPECFILE" --target "$rpm_target" + 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 943d4c86bbf..3c7a8a3d482 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,14 +1,13 @@ - 7.20.0 + 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 f07c621bbbd..086c726f88a 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -1,32 +1,39 @@ - - true - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + true + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/GlobalHotKeys b/v2rayN/GlobalHotKeys index ffb2850df09..569a95bb0fd 160000 --- a/v2rayN/GlobalHotKeys +++ b/v2rayN/GlobalHotKeys @@ -1 +1 @@ -Subproject commit ffb2850df0991495d0918e13cc5701737f26175a +Subproject commit 569a95bb0fd2280d8d5581250aae54ecc2122d10 diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs new file mode 100644 index 00000000000..105310927c1 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs @@ -0,0 +1,114 @@ +using AwesomeAssertions; +using ServiceLib.Enums; +using ServiceLib.Handler.Builder; +using ServiceLib.Helper; +using ServiceLib.Models; +using Xunit; + +namespace ServiceLib.Tests.CoreConfig.Context; + +public class CoreConfigContextBuilderTests +{ + [Fact] + public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError() + { + var config = CoreConfigTestFactory.CreateConfig(); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var groupAId = NewId("group-a"); + var groupBId = NewId("group-b"); + var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]); + var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]); + + await UpsertProfilesAsync(groupA, groupB); + + var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray); + context.AllProxiesMap.Clear(); + + var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false); + + validatorResult.Success.Should().BeFalse(); + validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg)); + context.AllProxiesMap.Should().NotContainKey(groupA.IndexId); + context.AllProxiesMap.Should().NotContainKey(groupB.IndexId); + } + + [Fact] + public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError() + { + var config = CoreConfigTestFactory.CreateConfig(); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var groupAId = NewId("group-a"); + var groupBId = NewId("group-b"); + var groupCId = NewId("group-c"); + var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]); + var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupCId]); + var groupC = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupCId, "group-c", [groupAId]); + + await UpsertProfilesAsync(groupA, groupB, groupC); + + var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray); + context.AllProxiesMap.Clear(); + + var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false); + + validatorResult.Success.Should().BeFalse(); + validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg)); + context.AllProxiesMap.Should().NotContainKey(groupA.IndexId); + context.AllProxiesMap.Should().NotContainKey(groupB.IndexId); + context.AllProxiesMap.Should().NotContainKey(groupC.IndexId); + } + + [Fact] + public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild() + { + var config = CoreConfigTestFactory.CreateConfig(); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var groupAId = NewId("group-a"); + var groupBId = NewId("group-b"); + var leafId = NewId("leaf"); + var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId, leafId]); + var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]); + var leaf = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, leafId, "leaf"); + + await UpsertProfilesAsync(groupA, groupB, leaf); + + var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray); + context.AllProxiesMap.Clear(); + + var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false); + + validatorResult.Success.Should().BeTrue(); + validatorResult.Errors.Should().BeEmpty(); + validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg)); + + context.AllProxiesMap.Should().ContainKey(leaf.IndexId); + context.AllProxiesMap.Should().ContainKey(groupA.IndexId); + context.AllProxiesMap.Should().NotContainKey(groupB.IndexId); + groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId); + } + + private static string NewId(string prefix) + { + return $"{prefix}-{Guid.NewGuid():N}"; + } + + private static bool ContainsCycleDependencyMessage(string message) + { + return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase) + || message.Contains("循环依赖", StringComparison.Ordinal) + || message.Contains("循環依賴", StringComparison.Ordinal) + || message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase); + } + + private static async Task UpsertProfilesAsync(params ProfileItem[] profiles) + { + SQLiteHelper.Instance.CreateTable(); + foreach (var profile in profiles) + { + await SQLiteHelper.Instance.ReplaceAsync(profile); + } + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs new file mode 100644 index 00000000000..91fb5c6aa7d --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs @@ -0,0 +1,217 @@ +using System.Reflection; +using ServiceLib.Enums; +using ServiceLib.Manager; +using ServiceLib.Models; + +namespace ServiceLib.Tests.CoreConfig; + +internal static class CoreConfigTestFactory +{ + public static void BindAppManagerConfig(Config config) + { + var field = typeof(AppManager).GetField("_config", BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(AppManager.Instance, config); + } + + public static Config CreateConfig(ECoreType vmessCoreType = ECoreType.Xray) + { + return new Config + { + CoreBasicItem = new CoreBasicItem { Loglevel = "warning" }, + TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" }, + KcpItem = new KcpItem(), + GrpcItem = new GrpcItem(), + RoutingBasicItem = + new RoutingBasicItem + { + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + RoutingIndexId = string.Empty, + }, + GuiItem = new GUIItem { EnableStatistics = false, DisplayRealTimeSpeed = false, EnableLog = false }, + MsgUIItem = new MsgUIItem(), + UiItem = + new UIItem + { + CurrentLanguage = "en", + CurrentFontFamily = "sans", + MainColumnItem = [], + WindowSizeItem = [] + }, + ConstItem = new ConstItem(), + SpeedTestItem = new SpeedTestItem + { + SpeedPingTestUrl = Global.SpeedPingTestUrls.First(), + SpeedTestUrl = Global.SpeedTestUrls.First(), + SpeedTestTimeout = 10, + MixedConcurrencyCount = 1, + IPAPIUrl = string.Empty, + }, + Mux4RayItem = new Mux4RayItem { Concurrency = 8, XudpConcurrency = 16, XudpProxyUDP443 = "reject" }, + Mux4SboxItem = new Mux4SboxItem { Protocol = Global.SingboxMuxs.First(), MaxConnections = 8 }, + HysteriaItem = new HysteriaItem { UpMbps = 100, DownMbps = 100 }, + ClashUIItem = new ClashUIItem { ConnectionsColumnItem = [] }, + SystemProxyItem = + new SystemProxyItem + { + SystemProxyExceptions = string.Empty, + SystemProxyAdvancedProtocol = string.Empty + }, + WebDavItem = new WebDavItem(), + CheckUpdateItem = new CheckUpdateItem(), + Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Length = "100-200", Interval = "10-20" }, + Inbound = + [ + new InItem + { + Protocol = nameof(EInboundProtocol.socks), + LocalPort = 10808, + UdpEnabled = true, + SniffingEnabled = true, + RouteOnly = false, + DestOverride = ["http", "tls"], + } + ], + GlobalHotkeys = [], + CoreTypeItem = + [ + new CoreTypeItem { ConfigType = EConfigType.VMess, CoreType = vmessCoreType } + ], + SimpleDNSItem = new SimpleDNSItem + { + BootstrapDNS = Global.DomainPureIPDNSAddress.FirstOrDefault(), + ServeStale = false, + ParallelQuery = false, + Strategy4Freedom = Global.AsIs, + Strategy4Proxy = Global.AsIs, + }, + IndexId = string.Empty, + SubIndexId = string.Empty, + }; + } + + public static ProfileItem CreateVmessNode(ECoreType coreType, string indexId = "node-1", string remarks = "demo") + { + var node = new ProfileItem + { + IndexId = indexId, + ConfigType = EConfigType.VMess, + CoreType = coreType, + Remarks = remarks, + Address = "example.com", + Port = 443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.raw), + StreamSecurity = string.Empty, + Subid = string.Empty, + }; + + node.SetProtocolExtra(node.GetProtocolExtra() with { AlterId = "0", VmessSecurity = Global.DefaultSecurity, }); + + return node; + } + + public static ProfileItem CreateSocksNode(ECoreType coreType, string indexId = "node-socks-1", + string remarks = "demo-socks") + { + return new ProfileItem + { + IndexId = indexId, + ConfigType = EConfigType.SOCKS, + CoreType = coreType, + Remarks = remarks, + Address = "127.0.0.1", + Port = 1080, + Password = "pass", + Username = "user", + Network = nameof(ETransport.raw), + StreamSecurity = string.Empty, + Subid = string.Empty, + }; + } + + public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks, + IEnumerable childIndexIds) + { + var node = new ProfileItem + { + IndexId = indexId, + ConfigType = EConfigType.PolicyGroup, + CoreType = coreType, + Remarks = remarks, + }; + node.SetProtocolExtra(node.GetProtocolExtra() with + { + GroupType = nameof(EConfigType.PolicyGroup), + ChildItems = string.Join(",", childIndexIds), + }); + + return node; + } + + public static ProfileItem CreateProxyChainNode(ECoreType coreType, string indexId, string remarks, + IEnumerable childIndexIds) + { + var node = new ProfileItem + { + IndexId = indexId, + ConfigType = EConfigType.ProxyChain, + CoreType = coreType, + Remarks = remarks, + }; + node.SetProtocolExtra(node.GetProtocolExtra() with + { + GroupType = nameof(EConfigType.ProxyChain), + ChildItems = string.Join(",", childIndexIds), + }); + + return node; + } + + public static CoreConfigContext CreateContext(Config config, ProfileItem node, ECoreType runCoreType) + { + return new CoreConfigContext + { + Node = node, + RunCoreType = runCoreType, + AppConfig = config, + RoutingItem = new RoutingItem + { + Id = "r1", + Remarks = "default", + RuleSet = "[]", + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + }, + RawDnsItem = null, + SimpleDnsItem = config.SimpleDNSItem, + AllProxiesMap = new Dictionary { [node.IndexId] = node }, + FullConfigTemplate = null, + IsTunEnabled = config.TunModeItem.EnableTun, + ProtectDomainList = [], + }; + } + + public static Config CreateConfigWithDirectExpectedIPs(ECoreType coreType, + string directExpectedIPs = "192.168.0.0/16,geoip:cn") + { + var config = CreateConfig(coreType); + config.SimpleDNSItem.DirectExpectedIPs = directExpectedIPs; + return config; + } + + public static Config CreateConfigWithBootstrapDNS(ECoreType coreType, string bootstrapDns = "8.8.8.8") + { + var config = CreateConfig(coreType); + 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/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs new file mode 100644 index 00000000000..158ba239191 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs @@ -0,0 +1,560 @@ +using AwesomeAssertions; +using ServiceLib.Common; +using ServiceLib.Enums; +using ServiceLib.Manager; +using ServiceLib.Models; +using ServiceLib.Services.CoreConfig; +using Xunit; + +namespace ServiceLib.Tests.CoreConfig.Singbox; + +public class CoreConfigSingboxServiceTests +{ + [Fact] + public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box); + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + result.Data.Should().NotBeNull(); + + var singboxConfig = JsonUtils.Deserialize(result.Data!.ToString()); + singboxConfig.Should().NotBeNull(); + singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks"); + singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed)); + } + + [Fact] + public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box); + node.Address = Global.Loopback; + node.Port = 1080; + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + IsTunEnabled = true, + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.inbounds.Should().Contain(i => + i.type == nameof(EInboundProtocol.mixed) + && i.listen == Global.Loopback + && i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks)); + cfg.inbounds.Should().Contain(i => i.type == "tun"); + } + + [Fact] + public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.CoreBasicItem.BindInterface = "eth0"; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + IsTunEnabled = true, + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag); + + proxy.bind_interface.Should().Be("eth0"); + proxy.detour.Should().BeNullOrEmpty(); + } + + [Fact] + public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2"); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.sing_box, "g1", "group", + [n1.IndexId, n2.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.sing_box); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[group.IndexId] = group; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); + cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); + } + + [Fact] + public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2"); + var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain", + [n1.IndexId, n2.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.sing_box); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[chain.IndexId] = chain; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks"); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => + o.tag == Global.ProxyTag && + (o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal)); + } + + [Fact] + public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2"); + var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n3", "node-3"); + var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain", + [n1.IndexId, n2.IndexId]); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.sing_box, "g1", "group", + [chain.IndexId, n3.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.sing_box); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[n3.IndexId] = n3; + context.AllProxiesMap[chain.IndexId] = chain; + context.AllProxiesMap[group.IndexId] = group; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); + cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); + } + + [Fact] + public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n2", "node-2"); + var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n3", "node-3"); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.sing_box, "g1", "group", + [n1.IndexId, n2.IndexId]); + var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.sing_box, "c1", "chain", + [group.IndexId, n3.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.sing_box); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[n3.IndexId] = n3; + context.AllProxiesMap[group.IndexId] = group; + context.AllProxiesMap[chain.IndexId] = chain; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector"); + cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest"); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal)); + + var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)); + proxyCloneCount.Should().Be(2); + + var allCloneDetoursPointToGroupBranches = cfg.outbounds + .Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)) + .All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal)); + allCloneDetoursPointToGroupBranches.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-split-1", + Remarks = "split-direct-block", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = Global.DirectTag, + Domain = ["full:direct.example.com"], + }, + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = Global.BlockTag, + Domain = ["full:block.example.com"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + var hasDirectRule = cfg.route.rules.Any(r => + r.domain != null + && r.domain.Contains("direct.example.com") + && r.outbound == Global.DirectTag); + hasDirectRule.Should().BeTrue(); + + var hasBlockRule = cfg.route.rules.Any(r => + r.domain != null + && r.domain.Contains("block.example.com") + && r.action == "reject"); + hasBlockRule.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var routeNode = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-route", "route-node"); + + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-split-2", + Remarks = "split-remark", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = routeNode.Remarks, + Domain = ["full:route.example.com"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + context.AllProxiesMap[$"remark:{routeNode.Remarks}"] = routeNode; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}"; + + cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal)); + + var hasRouteRule = cfg.route.rules.Any(r => + r.domain != null + && r.domain.Contains("route.example.com") + && (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal)); + hasRouteRule.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule() + { + var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs( + ECoreType.sing_box, + "192.168.0.0/16,geoip:cn"); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-expected", + Remarks = "dns-direct-expected", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.DNS, + OutboundTag = Global.DirectTag, + Domain = ["geosite:cn"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + var hasExpectedRule = cfg.dns.rules?.Any(r => + r.server == Global.SingboxDirectDNSTag + && r.ip_cidr?.Contains("192.168.0.0/16") == true + && r.rule_set?.Contains("geosite-cn") == true + && r.rule_set?.Contains("geoip-cn") == true) ?? false; + + hasExpectedRule.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver() + { + var bootstrapDns = "8.8.8.8"; + var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box); + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns); + + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var bootstrapServer = cfg.dns.servers?.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag); + bootstrapServer.Should().NotBeNull(); + (bootstrapServer?.server ?? string.Empty).Should().Contain(bootstrapDns); + } + + [Fact] + public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.SimpleDNSItem.DirectDNS = "1.1.1.1"; + config.SimpleDNSItem.RemoteDNS = "9.9.9.9"; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-direct-final", + Remarks = "direct-final", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = Global.DirectTag, + Ip = ["0.0.0.0/0"], + Port = "0-65535", + Network = "tcp,udp", + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag); + } + + [Fact] + public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule() + { + var config = + CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn"); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-unmatched", + Remarks = "dns-direct-unmatched", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.DNS, + OutboundTag = Global.DirectTag, + Domain = ["geosite:us"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + var hasExpectedRule = cfg.dns.rules?.Any(r => + r.server == Global.SingboxDirectDNSTag + && r.ip_cidr?.Contains("192.168.0.0/16") == true + && r.rule_set?.Contains("geoip-cn") == true) ?? false; + hasExpectedRule.Should().BeFalse(); + } + + [Theory] + [InlineData("geosite:cn", "geosite-cn")] + [InlineData("geosite:geolocation-cn", "geosite-geolocation-cn")] + [InlineData("geosite:tld-cn", "geosite-tld-cn")] + public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag, + string expectedRuleSetTag) + { + var config = + CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn"); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-variant", + Remarks = "dns-direct-variant", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, RuleType = ERuleType.DNS, OutboundTag = Global.DirectTag, Domain = [domainTag], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + var hasExpectedRule = cfg.dns.rules?.Any(r => + r.server == Global.SingboxDirectDNSTag + && r.ip_cidr?.Contains("192.168.0.0/16") == true + && r.rule_set?.Contains(expectedRuleSetTag) == true + && r.rule_set?.Contains("geoip-cn") == true) ?? false; + hasExpectedRule.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1"; + config.SimpleDNSItem.DirectDNS = "https://resolver.example/dns-query"; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box); + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag); + hostsServer.Should().NotBeNull(); + hostsServer!.predefined.Should().ContainKey("resolver.example"); + hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1"); + + var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag); + directServer.Should().NotBeNull(); + directServer!.domain_resolver.Should().Be(Global.SingboxHostsDNSTag); + } + + [Fact] + public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box, "n-main", "main"); + var rawDns = new Dns4Sbox + { + servers = + [ + new Server4Sbox { tag = "remote", type = "udp", server = "8.8.8.8", detour = Global.ProxyTag, } + ], + rules = [], + }; + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with + { + RawDnsItem = new DNSItem + { + Id = "dns-raw-1", + Remarks = "raw", + Enabled = true, + CoreType = ECoreType.sing_box, + NormalDNS = JsonUtils.Serialize(rawDns), + DomainDNSAddress = "1.1.1.1", + } + }; + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8"); + cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag); + cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Global.ToString()); + cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString()); + } +} diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs new file mode 100644 index 00000000000..4847050d8c1 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -0,0 +1,562 @@ +using AwesomeAssertions; +using ServiceLib.Common; +using ServiceLib.Enums; +using ServiceLib.Models; +using ServiceLib.Services.CoreConfig; +using Xunit; + +namespace ServiceLib.Tests.CoreConfig.V2ray; + +public class CoreConfigV2rayServiceTests +{ + [Fact] + public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + + var v2rayConfig = JsonUtils.Deserialize(result.Data!.ToString()); + v2rayConfig.Should().NotBeNull(); + v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess"); + v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed)); + } + + [Fact] + public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2"); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group", + [n1.IndexId, n2.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[group.IndexId] = group; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); + cfg.routing.balancers.Should().NotBeNull(); + cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); + } + + [Fact] + public void GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2"); + var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.Xray, "c1", "chain", [n1.IndexId, n2.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.Xray); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[chain.IndexId] = chain; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); + var hasDialerChain = cfg.outbounds.Any(o => + o.tag == Global.ProxyTag + && o.streamSettings is not null + && o.streamSettings.sockopt is not null + && (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-", + StringComparison.Ordinal)); + hasDialerChain.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2"); + var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n3", "node-3"); + var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.Xray, "c1", "chain", [n1.IndexId, n2.IndexId]); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group", + [chain.IndexId, n3.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[n3.IndexId] = n3; + context.AllProxiesMap[chain.IndexId] = chain; + context.AllProxiesMap[group.IndexId] = group; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal)); + cfg.routing.balancers.Should().NotBeNull(); + cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); + } + + [Fact] + public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var n1 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n1", "node-1"); + var n2 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n2", "node-2"); + var n3 = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n3", "node-3"); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "g1", "group", + [n1.IndexId, n2.IndexId]); + var chain = CoreConfigTestFactory.CreateProxyChainNode(ECoreType.Xray, "c1", "chain", + [group.IndexId, n3.IndexId]); + + var context = CoreConfigTestFactory.CreateContext(config, chain, ECoreType.Xray); + context.AllProxiesMap[n1.IndexId] = n1; + context.AllProxiesMap[n2.IndexId] = n2; + context.AllProxiesMap[n3.IndexId] = n3; + context.AllProxiesMap[group.IndexId] = group; + context.AllProxiesMap[chain.IndexId] = chain; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal)); + cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal)); + + var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)); + proxyCloneCount.Should().Be(2); + + var allCloneDialersPointToGroupBranches = cfg.outbounds + .Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal)) + .All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-", + StringComparison.Ordinal)); + allCloneDialersPointToGroupBranches.Should().BeTrue(); + + cfg.routing.balancers.Should().NotBeNull(); + cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix); + } + + [Fact] + public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RoutingItem = new RoutingItem + { + Id = "r-split-1", + Remarks = "split-direct-block", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = Global.DirectTag, + Domain = ["full:direct.example.com"], + }, + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = Global.BlockTag, + Domain = ["full:block.example.com"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + + var hasDirectRule = cfg.routing.rules.Any(r => + r.domain != null + && r.domain.Contains("full:direct.example.com") + && r.outboundTag == Global.DirectTag); + hasDirectRule.Should().BeTrue(); + + var hasBlockRule = cfg.routing.rules.Any(r => + r.domain != null + && r.domain.Contains("full:block.example.com") + && r.outboundTag == Global.BlockTag); + hasBlockRule.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var routeNode = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "n-route", "route-node"); + + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RoutingItem = new RoutingItem + { + Id = "r-split-2", + Remarks = "split-remark", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = routeNode.Remarks, + Domain = ["full:route.example.com"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + context.AllProxiesMap[$"remark:{routeNode.Remarks}"] = routeNode; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}"; + + cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal)); + var hasRouteRule = cfg.routing.rules.Any(r => + r.domain != null + && r.domain.Contains("full:route.example.com") + && (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal)); + hasRouteRule.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer() + { + var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-expected", + Remarks = "dns-direct-expected", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.DNS, + OutboundTag = Global.DirectTag, + Domain = ["geosite:cn"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .ToList(); + + var hasExpectedServer = dnsServers.Any(s => + (s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal) + && s.domains?.Contains("geosite:cn") == true + && s.expectedIPs?.Contains("192.168.0.0/16") == true + && s.expectedIPs?.Contains("geoip:cn") == true); + hasExpectedServer.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains() + { + var bootstrapDns = "8.8.8.8"; + var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns); + config.SimpleDNSItem.DirectDNS = "https://dns-direct.example/dns-query"; + config.SimpleDNSItem.RemoteDNS = "https://dns-remote.example/dns-query"; + 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 dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .ToList(); + + var hasBootstrapServer = dnsServers.Any(s => + s.address == bootstrapDns + && s.domains?.Contains("full:dns-direct.example") == true + && s.domains?.Contains("full:dns-remote.example") == true); + hasBootstrapServer.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + config.SimpleDNSItem.DirectDNS = "1.1.1.1"; + config.SimpleDNSItem.RemoteDNS = "9.9.9.9"; + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RoutingItem = new RoutingItem + { + Id = "r-direct-final", + Remarks = "direct-final", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.Routing, + OutboundTag = Global.DirectTag, + Ip = ["0.0.0.0/0"], + Port = "0-65535", + Network = "tcp,udp", + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .ToList(); + + var hasDirectFallback = dnsServers.Any(s => + (s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal) + && s.address == "1.1.1.1"); + hasDirectFallback.Should().BeTrue(); + + var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9"); + hasRemoteFallback.Should().BeFalse(); + } + + [Fact] + public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs() + { + var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-unmatched", + Remarks = "dns-direct-unmatched", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, + RuleType = ERuleType.DNS, + OutboundTag = Global.DirectTag, + Domain = ["geosite:us"], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .ToList(); + + var hasExpectedIPs = dnsServers.Any(s => + s.expectedIPs?.Contains("192.168.0.0/16") == true + || s.expectedIPs?.Contains("geoip:cn") == true); + hasExpectedIPs.Should().BeFalse(); + } + + [Theory] + [InlineData("geosite:cn")] + [InlineData("geosite:geolocation-cn")] + [InlineData("geosite:tld-cn")] + public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag) + { + var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn"); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RoutingItem = new RoutingItem + { + Id = "r-dns-direct-variant", + Remarks = "dns-direct-variant", + RuleSet = JsonUtils.Serialize(new List + { + new() + { + Enabled = true, RuleType = ERuleType.DNS, OutboundTag = Global.DirectTag, Domain = [domainTag], + } + }), + DomainStrategy = Global.AsIs, + DomainStrategy4Singbox = string.Empty, + } + }; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + var dnsServers = dns.servers + .Select(s => JsonUtils.Deserialize(JsonUtils.Serialize(s))) + .Where(s => s is not null) + .Cast() + .ToList(); + + var hasExpectedServer = dnsServers.Any(s => + (s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal) + && s.domains?.Contains(domainTag) == true + && s.expectedIPs?.Contains("192.168.0.0/16") == true + && s.expectedIPs?.Contains("geoip:cn") == true); + hasExpectedServer.Should().BeTrue(); + } + + [Fact] + public void GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1"; + 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 dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + + dns.hosts.Should().NotBeNull(); + dns.hosts!.Should().ContainKey("resolver.example"); + JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1"); + } + + [Fact] + public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main"); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray) with + { + RawDnsItem = new DNSItem + { + Id = "dns-raw-1", + Remarks = "raw", + Enabled = true, + CoreType = ECoreType.Xray, + NormalDNS = "{\"servers\":[\"8.8.8.8\"],\"hosts\":{\"raw.example\":\"1.1.1.1\"}}", + DomainStrategy4Freedom = "UseIPv4", + } + }; + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var dns = JsonUtils.Deserialize(JsonUtils.Serialize(cfg.dns))!; + + JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8"); + dns.hosts.Should().NotBeNull(); + dns.hosts!.Should().ContainKey("raw.example"); + JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1"); + + var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom"); + 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/Fmt/FmtHandlerTests.cs b/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs new file mode 100644 index 00000000000..143fa3e5a2b --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs @@ -0,0 +1,173 @@ +using AwesomeAssertions; +using ServiceLib.Enums; +using ServiceLib.Handler.Fmt; +using ServiceLib.Models; +using Xunit; + +namespace ServiceLib.Tests.Fmt; + +public class FmtHandlerTests +{ + [Fact] + public void GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields() + { + var source = CreateVmessProfile(); + + var resolved = ExportThenImport(source); + + resolved.ConfigType.Should().Be(EConfigType.VMess); + resolved.Remarks.Should().Be(source.Remarks); + resolved.Address.Should().Be(source.Address); + resolved.Port.Should().Be(source.Port); + resolved.Password.Should().Be(source.Password); + resolved.GetProtocolExtra().AlterId.Should().Be(source.GetProtocolExtra().AlterId); + } + + [Fact] + public void GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields() + { + var source = CreateVlessProfile(); + + var resolved = ExportThenImport(source); + + resolved.ConfigType.Should().Be(EConfigType.VLESS); + resolved.Remarks.Should().Be(source.Remarks); + resolved.Address.Should().Be(source.Address); + resolved.Port.Should().Be(source.Port); + resolved.Password.Should().Be(source.Password); + resolved.GetProtocolExtra().VlessEncryption.Should().Be(Global.None); + } + + [Fact] + public void GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields() + { + var source = CreateShadowsocksProfile(); + + var resolved = ExportThenImport(source); + + resolved.ConfigType.Should().Be(EConfigType.Shadowsocks); + resolved.Remarks.Should().Be(source.Remarks); + resolved.Address.Should().Be(source.Address); + resolved.Port.Should().Be(source.Port); + resolved.Password.Should().Be(source.Password); + resolved.GetProtocolExtra().SsMethod.Should().Be(source.GetProtocolExtra().SsMethod); + } + + [Fact] + public void GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields() + { + var source = CreateSocksProfile(); + + var resolved = ExportThenImport(source); + + resolved.ConfigType.Should().Be(EConfigType.SOCKS); + resolved.Remarks.Should().Be(source.Remarks); + resolved.Address.Should().Be(source.Address); + resolved.Port.Should().Be(source.Port); + resolved.Username.Should().Be(source.Username); + resolved.Password.Should().Be(source.Password); + } + + [Fact] + public void ResolveConfig_UnsupportedProtocol_ShouldReturnNull() + { + var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg); + + resolved.Should().BeNull(); + msg.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public void GetShareUri_UnsupportedConfigType_ShouldReturnNull() + { + var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", }; + + var uri = FmtHandler.GetShareUri(item); + + uri.Should().BeNull(); + } + + private static ProfileItem ExportThenImport(ProfileItem source) + { + var uri = FmtHandler.GetShareUri(source); + + uri.Should().NotBeNullOrWhiteSpace(); + uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should() + .BeTrue(); + + var resolved = FmtHandler.ResolveConfig(uri, out var msg); + + resolved.Should().NotBeNull($"uri: {uri}, msg: {msg}"); + return resolved!; + } + + private static ProfileItem CreateVmessProfile() + { + var item = new ProfileItem + { + ConfigType = EConfigType.VMess, + Remarks = "vmess demo", + Address = "example.com", + Port = 443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.raw), + StreamSecurity = string.Empty, + }; + + item.SetProtocolExtra(new ProtocolExtraItem { AlterId = "0", VmessSecurity = Global.DefaultSecurity, }); + item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, }); + + return item; + } + + private static ProfileItem CreateVlessProfile() + { + var item = new ProfileItem + { + ConfigType = EConfigType.VLESS, + Remarks = "vless demo", + Address = "vless.example", + Port = 8443, + Password = Guid.NewGuid().ToString(), + Network = nameof(ETransport.raw), + StreamSecurity = string.Empty, + }; + + item.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None, }); + item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, }); + + return item; + } + + private static ProfileItem CreateShadowsocksProfile() + { + var item = new ProfileItem + { + ConfigType = EConfigType.Shadowsocks, + Remarks = "ss demo", + Address = "1.2.3.4", + Port = 8388, + Password = "pass123", + Network = nameof(ETransport.raw), + StreamSecurity = string.Empty, + }; + + item.SetProtocolExtra(new ProtocolExtraItem { SsMethod = "aes-128-gcm", }); + item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, }); + + return item; + } + + private static ProfileItem CreateSocksProfile() + { + return new ProfileItem + { + ConfigType = EConfigType.SOCKS, + Remarks = "socks demo", + Address = "127.0.0.1", + Port = 1080, + Username = "user", + Password = "pass", + }; + } +} diff --git a/v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs b/v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs new file mode 100644 index 00000000000..91df67f4cbc --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs @@ -0,0 +1,37 @@ +using AwesomeAssertions; +using ServiceLib.Enums; +using ServiceLib.Handler.Fmt; +using ServiceLib.Tests.CoreConfig; +using Xunit; + +namespace ServiceLib.Tests.Fmt; + +public class InnerFmtTests +{ + [Fact] + public void ToUriAndResolve_ShouldRoundTripPolicyGroupReferences() + { + var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a"); + var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b"); + var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "group-1", "group-1", + [childA.IndexId, childB.IndexId]); + group.SetProtocolExtra(group.GetProtocolExtra() with { SubChildItems = "original-sub" }); + + var uri = InnerFmt.ToUri([group, childA, childB]); + + uri.Should().NotBeNullOrWhiteSpace(); + + var resolved = InnerFmt.Resolve(uri!, "sub-123"); + + resolved.Should().NotBeNull(); + resolved.Should().HaveCount(3); + + var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks); + var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks); + var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks); + + resolvedGroup.ConfigType.Should().Be(EConfigType.PolicyGroup); + resolvedGroup.GetProtocolExtra().SubChildItems.Should().Be("sub-123"); + resolvedGroup.GetProtocolExtra().ChildItems.Should().Be($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}"); + } +} diff --git a/v2rayN/ServiceLib.Tests/Fmt/WireguardFmtTests.cs b/v2rayN/ServiceLib.Tests/Fmt/WireguardFmtTests.cs new file mode 100644 index 00000000000..717fa1bc567 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/WireguardFmtTests.cs @@ -0,0 +1,47 @@ +using AwesomeAssertions; +using ServiceLib.Handler.Fmt; +using Xunit; + +namespace ServiceLib.Tests.Fmt; + +public class WireguardFmtTests +{ + [Fact] + public void ResolveConfig_ShouldParsePeersAndIgnoreInlineComments() + { + const string config = + """ + [Interface] + PrivateKey = interface-private-key + Address = 10.0.0.2/32, fd00::2/128 ; inline comment + MTU = 1420 + + [Peer] + PublicKey = peer-public-key + PresharedKey = peer-preshared-key + Reserved = 1, 2, 3 # inline comment + Endpoint = [2001:db8::1]:51820 # inline comment + + [Peer] + PublicKey = peer-public-key-2 + Endpoint = example.com:12345 + """; + + var resolved = WireguardFmt.ResolveConfig(config); + + resolved.Should().NotBeNull(); + resolved.Should().HaveCount(2); + + var first = resolved![0]; + first.Address.Should().Be("2001:db8::1"); + first.Port.Should().Be(51820); + first.Password.Should().Be("interface-private-key"); + first.GetProtocolExtra().WgReserved.Should().Be("1, 2, 3"); + first.GetProtocolExtra().WgInterfaceAddress.Should().Be("10.0.0.2/32, fd00::2/128"); + first.GetProtocolExtra().WgMtu.Should().Be(1420); + + var second = resolved[1]; + second.Address.Should().Be("example.com"); + second.Port.Should().Be(12345); + } +} 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.Tests/ServiceLib.Tests.csproj b/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj new file mode 100644 index 00000000000..f86ca573fc9 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj @@ -0,0 +1,23 @@ + + + + Exe + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/v2rayN/ServiceLib.UdpTest/GlobalUsings.cs b/v2rayN/ServiceLib.UdpTest/GlobalUsings.cs new file mode 100644 index 00000000000..2ef96e1bc25 --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using System.Buffers.Binary; +global using System.Diagnostics; +global using System.Net; +global using System.Net.Sockets; +global using System.Text; diff --git a/v2rayN/ServiceLib.UdpTest/ServiceLib.UdpTest.csproj b/v2rayN/ServiceLib.UdpTest/ServiceLib.UdpTest.csproj new file mode 100644 index 00000000000..9a69e670cbc --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/ServiceLib.UdpTest.csproj @@ -0,0 +1,7 @@ + + + + Library + + + diff --git a/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs new file mode 100644 index 00000000000..f9a2951f536 --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs @@ -0,0 +1,420 @@ +namespace ServiceLib.UdpTest; + +public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable +{ + private TcpClient _tcpClient; + private UdpClient _udpClient; + private IPEndPoint _relayEndPoint; + + private bool _initialized = false; + + /// + /// Send UDP data to a remote endpoint (IP address) + /// + public async Task SendAsync(IPEndPoint remote, byte[] data) + { + var addrData = new Socks5AddressData + { + AddressType = remote.Address.AddressFamily == AddressFamily.InterNetwork + ? Socks5AddressData.AddrTypeIPv4 + : Socks5AddressData.AddrTypeIPv6, + Host = remote.Address.ToString(), + Port = (ushort)remote.Port + }; + var packet = BuildSocks5UdpPacket(addrData, data); + await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); + } + + /// + /// Send UDP data to a remote endpoint (domain name or IP address) + /// + /// Domain name or IP address + /// Port number + /// Data to send + public async Task SendAsync(string host, ushort port, byte[] data) + { + var addrData = new Socks5AddressData(); + + // Try to parse as IP address first + if (IPAddress.TryParse(host, out var ipAddr)) + { + addrData.AddressType = ipAddr.AddressFamily == AddressFamily.InterNetwork + ? Socks5AddressData.AddrTypeIPv4 + : Socks5AddressData.AddrTypeIPv6; + addrData.Host = ipAddr.ToString(); + } + else + { + // Treat as domain name + addrData.AddressType = Socks5AddressData.AddrTypeDomain; + addrData.Host = host; + } + + addrData.Port = port; + + var packet = BuildSocks5UdpPacket(addrData, data); + await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); + } + + /// + /// Receive UDP data from remote endpoint + /// + /// Cancellation token to cancel the receive operation + /// Remote endpoint information and received data + public async Task<(Socks5RemoteEndpoint Remote, byte[] Data)> ReceiveAsync( + CancellationToken cancellationToken = default) + { + var result = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var (remote, payload) = ParseSocks5UdpPacket(result.Buffer); + return (remote, payload); + } + + /// + /// Represents a remote endpoint that can be either an IP address or a domain name + /// + public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain) + { + public string Host { get; set; } = host; + public ushort Port { get; set; } = port; + public bool IsDomain { get; set; } = isDomain; + } + + private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data) + { + using var ms = new MemoryStream(); + + // RSV (2 bytes) + FRAG (1 byte) - Reserved and Fragment fields + ms.WriteByte(0x00); + ms.WriteByte(0x00); + ms.WriteByte(0x00); + + // Write address (ATYP + address + port) + ms.Write(addressData.ToBytes()); + + // User data payload + ms.Write(data); + + return ms.ToArray(); + } + + private static (Socks5RemoteEndpoint Remote, byte[] Data) ParseSocks5UdpPacket(byte[] packet) + { + if (packet.Length < 10) // Minimum length: RSV(2) + FRAG(1) + ATYP(1) + IPv4(4) + Port(2) = 10 + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: too short"); + } + + var offset = 0; + + // RSV (2 bytes) - Reserved field, skip + offset += 2; + + // FRAG (1 byte) - Fragment number, currently only support 0 (no fragmentation) + var frag = packet[offset++]; + if (frag != 0x00) + { + throw new NotSupportedException("SOCKS5 UDP fragmentation is not supported"); + } + + // ATYP (1 byte) - Address type + var addressType = packet[offset++]; + + string host; + int addressLength; + bool isDomain; + + switch (addressType) + { + case Socks5AddressData.AddrTypeIPv4: + if (packet.Length < offset + 4) + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: IPv4 address incomplete"); + } + + var ipv4Bytes = new byte[4]; + Array.Copy(packet, offset, ipv4Bytes, 0, 4); + host = new IPAddress(ipv4Bytes).ToString(); + addressLength = 4; + isDomain = false; + break; + + case Socks5AddressData.AddrTypeIPv6: + if (packet.Length < offset + 16) + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: IPv6 address incomplete"); + } + + var ipv6Bytes = new byte[16]; + Array.Copy(packet, offset, ipv6Bytes, 0, 16); + host = new IPAddress(ipv6Bytes).ToString(); + addressLength = 16; + isDomain = false; + break; + + case Socks5AddressData.AddrTypeDomain: + if (packet.Length < offset + 1) + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: domain length missing"); + } + + var domainLength = packet[offset++]; + if (packet.Length < offset + domainLength) + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: domain incomplete"); + } + + host = Encoding.ASCII.GetString(packet, offset, domainLength); + addressLength = domainLength; + isDomain = true; + break; + + default: + throw new NotSupportedException($"Unsupported SOCKS5 address type: {addressType}"); + } + + offset += addressLength; + + // Port (2 bytes, big-endian) + if (packet.Length < offset + 2) + { + throw new ArgumentException("Invalid SOCKS5 UDP packet: port incomplete"); + } + + var port = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(offset, 2)); + offset += 2; + + // Data (remaining bytes) + var dataLength = packet.Length - offset; + var data = new byte[dataLength]; + if (dataLength > 0) + { + Array.Copy(packet, offset, data, 0, dataLength); + } + + // Create remote endpoint without DNS resolution + var remote = new Socks5RemoteEndpoint(host, port, isDomain); + return (remote, data); + } + + public void Dispose() + { + _tcpClient.Dispose(); + _udpClient.Dispose(); + } + + #region SOCKS5 Connection Handling + + private const byte Socks5Version = 0x05; + private const byte SocksCmdUdpAssociate = 0x03; + + public async Task EstablishUdpAssociationAsync(CancellationToken cancellationToken) + { + if (_initialized) + { + Dispose(); + _initialized = false; + } + + _udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); + _tcpClient = new TcpClient(); + try + { + await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false); + } + catch (SocketException) + { + return false; + } + + var tcpControlStream = _tcpClient.GetStream(); + + byte[] handshakeRequest = [Socks5Version, 0x01, 0x00]; + await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false); + var handshakeResponse = new byte[2]; + if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 || + handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00) + { + return false; + } + + var clientAddrForSocks = new Socks5AddressData + { + AddressType = Socks5AddressData.AddrTypeIPv4, + Host = "0.0.0.0", + Port = 0 + }; + using var udpAssociateReqMs = new MemoryStream(); + udpAssociateReqMs.WriteByte(Socks5Version); + udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate); + udpAssociateReqMs.WriteByte(0x00); + udpAssociateReqMs.Write(clientAddrForSocks.ToBytes()); + await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false); + + var verRepRsv = new byte[3]; + if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 || + verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00) + { + return false; + } + + var proxyRelaySocksAddr = + await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false); + if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp)) + { + return false; + } + + _relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port); + _initialized = true; + return true; + } + + #endregion SOCKS5 Connection Handling + + #region SOCKS5 Address Handling + + private class Socks5AddressData + { + public const byte AddrTypeIPv4 = 0x01; + public const byte AddrTypeDomain = 0x03; + public const byte AddrTypeIPv6 = 0x04; + + public byte AddressType { get; set; } + public string Host { get; set; } = string.Empty; + public ushort Port { get; set; } + + public byte[] ToBytes() + { + using var ms = new MemoryStream(); + ms.WriteByte(AddressType); + switch (AddressType) + { + case AddrTypeIPv4: + if (IPAddress.TryParse(Host, out var ip) && ip.AddressFamily == AddressFamily.InterNetwork) + { + ms.Write(ip.GetAddressBytes(), 0, 4); + } + else + { + ms.Write([0, 0, 0, 0]); + } + + break; + + case AddrTypeDomain: + if (string.IsNullOrEmpty(Host)) + { + ms.WriteByte(0); + } + else + { + var domainBytes = Encoding.ASCII.GetBytes(Host); + ms.WriteByte((byte)domainBytes.Length); + ms.Write(domainBytes); + } + + break; + + case AddrTypeIPv6: + if (IPAddress.TryParse(Host, out var ip6) && ip6.AddressFamily == AddressFamily.InterNetworkV6) + { + ms.Write(ip6.GetAddressBytes(), 0, 16); + } + else + { + ms.Write(new byte[16]); + } + + break; + + default: + throw new NotSupportedException($"SOCKS5 address type {AddressType} not supported."); + } + + var portBytes = new byte[2]; + BinaryPrimitives.WriteUInt16BigEndian(portBytes, Port); + ms.Write(portBytes); + return ms.ToArray(); + } + + public static async Task ParseAsync(Stream stream, CancellationToken ct) + { + var addr = new Socks5AddressData(); + var typeByte = new byte[1]; + try + { + if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1) + { + return null; + } + + addr.AddressType = typeByte[0]; + switch (addr.AddressType) + { + case AddrTypeIPv4: + var ipv4Bytes = new byte[4]; + if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4) + { + return null; + } + + addr.Host = new IPAddress(ipv4Bytes).ToString(); + break; + + case AddrTypeDomain: + var lenByte = new byte[1]; + if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1) + { + return null; + } + + if (lenByte[0] == 0) + { + addr.Host = string.Empty; + } + else + { + var domainBytes = new byte[lenByte[0]]; + if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct) + .ConfigureAwait(false) < domainBytes.Length) + { + return null; + } + + addr.Host = Encoding.ASCII.GetString(domainBytes); + } + + break; + + case AddrTypeIPv6: + var ipv6Bytes = new byte[16]; + if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16) + { + return null; + } + + addr.Host = new IPAddress(ipv6Bytes).ToString(); + break; + + default: + return null; + } + + var portBytes = new byte[2]; + if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2) + { + return null; + } + + addr.Port = BinaryPrimitives.ReadUInt16BigEndian(portBytes); + return addr; + } + catch (Exception ex) when (ex is IOException or ObjectDisposedException) + { + return null; + } + } + } + + #endregion SOCKS5 Address Handling +} diff --git a/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs b/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs new file mode 100644 index 00000000000..81bea2b83ff --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs @@ -0,0 +1,77 @@ +namespace ServiceLib.UdpTest.Tester; + +public class DnsService : IUdpTest +{ + private const int DnsDefaultPort = 53; + private const string DnsDefaultServer = "8.8.8.8"; // Google Public DNS + + private static readonly byte[] DnsQueryPacket = + [ + // Header: ID=0x1234, Standard query with RD set, QDCOUNT=1 + 0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // Question: www.google.com, Type A, Class IN + 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, + 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, + 0x00, 0x01, 0x00, 0x01 + ]; + + public byte[] BuildUdpRequestPacket() + { + return (byte[])DnsQueryPacket.Clone(); + } + + public bool VerifyAndExtractUdpResponse(byte[] dnsResponseBytes) + { + if (dnsResponseBytes.Length < 12) + { + return false; + } + + try + { + // Check transaction ID (should match 0x1234) + var transactionId = BinaryPrimitives.ReadUInt16BigEndian(dnsResponseBytes.AsSpan(0, 2)); + if (transactionId != 0x1234) + { + return false; + } + + // Check flags - should be a response (QR=1) + var flags = BinaryPrimitives.ReadUInt16BigEndian(dnsResponseBytes.AsSpan(2, 2)); + if ((flags & 0x8000) == 0) + { + return false; // Not a response + } + + // Check response code (RCODE) - should be 0 (no error) + if ((flags & 0x000F) != 0) + { + return false; // DNS error + } + + // Check answer count + var answerCount = BinaryPrimitives.ReadUInt16BigEndian(dnsResponseBytes.AsSpan(6, 2)); + if (answerCount == 0) + { + return false; // No answers + } + + return true; + } + catch + { + return false; + } + } + + public ushort GetDefaultTargetPort() + { + return DnsDefaultPort; + } + + public string GetDefaultTargetHost() + { + return DnsDefaultServer; + } +} diff --git a/v2rayN/ServiceLib.UdpTest/Tester/IUdpTest.cs b/v2rayN/ServiceLib.UdpTest/Tester/IUdpTest.cs new file mode 100644 index 00000000000..af3fa6c5b11 --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/Tester/IUdpTest.cs @@ -0,0 +1,12 @@ +namespace ServiceLib.UdpTest.Tester; + +public interface IUdpTest +{ + public byte[] BuildUdpRequestPacket(); + + public bool VerifyAndExtractUdpResponse(byte[] udpResponseBytes); + + public ushort GetDefaultTargetPort(); + + public string GetDefaultTargetHost(); +} diff --git a/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs b/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs new file mode 100644 index 00000000000..b4ec0221765 --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs @@ -0,0 +1,84 @@ +namespace ServiceLib.UdpTest.Tester; + +public class McBeService : IUdpTest +{ + private const int McBeDefaultPort = 19132; + private const string McBeDefaultServer = "pms.mc-complex.com"; + + // 0x01 | client alive time in ms (unsigned long long) | magic | client GUID + private static readonly byte[] McBeQueryPacket = + [ + // 0x01 + 0x01, + // Client alive time (1000 ms) + 0x27, 0xC4, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, + // Magic + 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78, + // Client GUID (random 16 bytes) + 0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E, + 0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B + ]; + + private static readonly byte[] McBeMagicBytes = + [ + 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78 + ]; + + private static readonly List ValidGameModes = + [ + "Survival", + "Creative", + "Adventure", + "Spectator" + ]; + + public byte[] BuildUdpRequestPacket() + { + return (byte[])McBeQueryPacket.Clone(); + } + + public bool VerifyAndExtractUdpResponse(byte[] mcbeResponseBytes) + { + // 0x1c | client alive time in ms (recorded from previous ping) | + // server GUID | Magic | string length | Edition + // + // Edition Example: + // + // MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133; + if (mcbeResponseBytes.Length < 48) + { + return false; + } + if (mcbeResponseBytes[0] != 0x1C) + { + return false; // Invalid packet type + } + var pongMagic = mcbeResponseBytes.Skip(17).Take(16).ToArray(); + if (!pongMagic.SequenceEqual(McBeMagicBytes)) + { + return false; // Magic bytes do not match + } + var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]); + var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray()); + var stringParts = stringData.Split(';'); + // check Game Mode str + var gameMode = stringParts.Length > 8 ? stringParts[8] : ""; + if (!ValidGameModes.Contains(gameMode)) + { + return false; // Invalid game mode + } + return true; + } + + public ushort GetDefaultTargetPort() + { + return McBeDefaultPort; + } + + public string GetDefaultTargetHost() + { + return McBeDefaultServer; + } +} diff --git a/v2rayN/ServiceLib.UdpTest/Tester/NtpService.cs b/v2rayN/ServiceLib.UdpTest/Tester/NtpService.cs new file mode 100644 index 00000000000..2421e4847dd --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/Tester/NtpService.cs @@ -0,0 +1,37 @@ +namespace ServiceLib.UdpTest.Tester; + +public class NtpService : IUdpTest +{ + private const int NtpDefaultPort = 123; + private const string NtpDefaultServer = "pool.ntp.org"; + + public byte[] BuildUdpRequestPacket() + { + var ntpReq = new byte[48]; + ntpReq[0] = 0x23; // LI=0, VN=4, Mode=3 + return ntpReq; + } + + public bool VerifyAndExtractUdpResponse(byte[] ntpResponseBytes) + { + if (ntpResponseBytes.Length < 48) + { + return false; + } + if ((ntpResponseBytes[0] & 0x07) != 4) + { + return false; + } + return true; + } + + public ushort GetDefaultTargetPort() + { + return NtpDefaultPort; + } + + public string GetDefaultTargetHost() + { + return NtpDefaultServer; + } +} diff --git a/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs b/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs new file mode 100644 index 00000000000..c6b925a316b --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs @@ -0,0 +1,52 @@ +namespace ServiceLib.UdpTest.Tester; + +public class StunService : IUdpTest +{ + private const int StunDefaultPort = 3478; + private const string StunDefaultServer = "stun.voztovoice.org"; + + private static readonly byte[] StunBindingRequestPacket = + [ + // STUN Binding Request + 0x00, 0x01, // Message Type: Binding Request (0x0001) + 0x00, 0x00, // Message Length: 0 (no attributes) + 0x21, 0x12, 0xA4, 0x42, // Magic Cookie: 0x2112A442 + // Transaction ID: 96 bits (12 bytes) random + 0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, + 0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, + ]; + + public byte[] BuildUdpRequestPacket() + { + return (byte[])StunBindingRequestPacket.Clone(); + } + + public bool VerifyAndExtractUdpResponse(byte[] stunResponseBytes) + { + if (stunResponseBytes.Length < 20) + { + return false; + } + + if (stunResponseBytes.Length >= 2) + { + var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1]; + if (messageType is 0x0101 or 0x0111) + { + return true; + } + } + + return true; + } + + public ushort GetDefaultTargetPort() + { + return StunDefaultPort; + } + + public string GetDefaultTargetHost() + { + return StunDefaultServer; + } +} diff --git a/v2rayN/ServiceLib.UdpTest/UdpTestService.cs b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs new file mode 100644 index 00000000000..be4b943d170 --- /dev/null +++ b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs @@ -0,0 +1,154 @@ +using ServiceLib.UdpTest.Tester; + +namespace ServiceLib.UdpTest; + +public class UdpTestService +{ + private const string DefaultUdpTestType = "ntp"; + private readonly IUdpTest _udpTest; + + private static readonly IReadOnlyDictionary> UdpTestFactories = + new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["ntp"] = () => new NtpService(), + ["dns"] = () => new DnsService(), + ["stun"] = () => new StunService(), + ["mcbe"] = () => new McBeService(), + }; + + private UdpTestService(IUdpTest udpTest) + { + _udpTest = udpTest; + } + + public static UdpTestService Create(string? udpTestType) + { + if (string.IsNullOrEmpty(udpTestType)) + { + return new UdpTestService(UdpTestFactories[DefaultUdpTestType]()); + } + + return UdpTestFactories.TryGetValue(udpTestType, out var factory) + ? new UdpTestService(factory()) + : new UdpTestService(UdpTestFactories[DefaultUdpTestType]()); + } + + public static UdpTestService CreateFromTarget(string? udpTestTarget, out string targetServerHost) + { + var parts = udpTestTarget?.Split(':', 2); + var udpTestType = parts?.Length > 0 ? parts[0] : DefaultUdpTestType; + + var udpService = Create(udpTestType); + targetServerHost = parts?.Length > 1 && !string.IsNullOrEmpty(parts[1]) + ? parts[1] + : udpService._udpTest.GetDefaultTargetHost(); + + return udpService; + } + + private (string host, ushort port) ParseHostAndPort(string targetServerHost) + { + if (string.IsNullOrEmpty(targetServerHost)) + { + return (_udpTest.GetDefaultTargetHost(), _udpTest.GetDefaultTargetPort()); + } + + // Handle IPv6 format: [::1]:port or [2001:db8::1]:port + if (targetServerHost.StartsWith('[')) + { + var closeBracketIndex = targetServerHost.IndexOf(']'); + if (closeBracketIndex > 0) + { + var host = targetServerHost.Substring(1, closeBracketIndex - 1); + if (closeBracketIndex < targetServerHost.Length - 1 && targetServerHost[closeBracketIndex + 1] == ':') + { + var portStr = targetServerHost.Substring(closeBracketIndex + 2); + if (ushort.TryParse(portStr, out var port)) + { + return (host, port); + } + } + return (host, _udpTest.GetDefaultTargetPort()); + } + } + + // Handle IPv4 or domain format: 1.1.1.1:53 or exam.com:333 + var lastColonIndex = targetServerHost.LastIndexOf(':'); + if (lastColonIndex > 0) + { + var host = targetServerHost.Substring(0, lastColonIndex); + var portStr = targetServerHost.Substring(lastColonIndex + 1); + if (ushort.TryParse(portStr, out var port)) + { + return (host, port); + } + } + + // No port specified, use default + return (targetServerHost, _udpTest.GetDefaultTargetPort()); + } + + public async Task SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout) + { + using var cts = new CancellationTokenSource(operationTimeout); + var cancellationToken = cts.Token; + var udpRequestPacket = _udpTest.BuildUdpRequestPacket(); + if (udpRequestPacket == null || udpRequestPacket.Length == 0) + { + throw new InvalidOperationException("Failed to build UDP request packet."); + } + using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port); + if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false)) + { + throw new Exception("Failed to establish UDP association with SOCKS5 proxy."); + } + + var (targetHost, targetPort) = ParseHostAndPort(targetServerHost); + + byte[] udpReceiveResult = null; + + // Get minimum round trip time from two attempts + var roundTripTime = TimeSpan.MaxValue; + + for (var attempt = 0; attempt < 2; attempt++) + { + try + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false); + var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false); + stopwatch.Stop(); + + udpReceiveResult = receiveResult; + + var currentRoundTripTime = stopwatch.Elapsed; + if (currentRoundTripTime < roundTripTime) + { + roundTripTime = currentRoundTripTime; + } + } + catch + { + if (attempt == 1 && roundTripTime == TimeSpan.MaxValue) + { + throw; + } + } + } + + if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2) + { + throw new Exception("Received NTP response is too short."); + } + + if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult)) + { + return roundTripTime; + } + else + { + throw new Exception("Failed to verify and extract UDP response."); + } + } +} 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/JsonUtils.cs b/v2rayN/ServiceLib/Common/JsonUtils.cs index 7e2b7f78829..951cdbd720a 100644 --- a/v2rayN/ServiceLib/Common/JsonUtils.cs +++ b/v2rayN/ServiceLib/Common/JsonUtils.cs @@ -17,6 +17,13 @@ public class JsonUtils Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + private static readonly JsonSerializerOptions _defaultSerializeNoIndentedOptions = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + private static readonly JsonSerializerOptions _nullValueSerializeOptions = new() { WriteIndented = true, @@ -24,6 +31,13 @@ public class JsonUtils Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + private static readonly JsonSerializerOptions _nullValueSerializeNoIndentedOptions = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + private static readonly JsonDocumentOptions _defaultDocumentOptions = new() { CommentHandling = JsonCommentHandling.Skip @@ -104,7 +118,13 @@ public static string Serialize(object? obj, bool indented = true, bool nullValue { return result; } - var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions; + var options = (nullValue, indented) switch + { + (true, true) => _nullValueSerializeOptions, + (true, false) => _nullValueSerializeNoIndentedOptions, + (false, true) => _defaultSerializeOptions, + _ => _defaultSerializeNoIndentedOptions + }; result = JsonSerializer.Serialize(obj, options); } catch (Exception ex) diff --git a/v2rayN/ServiceLib/Common/ProcUtils.cs b/v2rayN/ServiceLib/Common/ProcUtils.cs index ce487c7a0d4..3f5c31affa6 100644 --- a/v2rayN/ServiceLib/Common/ProcUtils.cs +++ b/v2rayN/ServiceLib/Common/ProcUtils.cs @@ -46,7 +46,7 @@ public static void ProcessStart(string? fileName, string arguments = "") return null; } - public static void RebootAsAdmin(bool blAdmin = true) + public static bool RebootAsAdmin(bool blAdmin = true) { try { @@ -58,11 +58,12 @@ public static void RebootAsAdmin(bool blAdmin = true) FileName = Utils.GetExePath().AppendQuotes(), Verb = blAdmin ? "runas" : null, }; - _ = Process.Start(startInfo); + return Process.Start(startInfo) != null; } catch (Exception ex) { Logging.SaveLog(_tag, ex); + return false; } } } diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 3907b1a73a2..9c2bd4af43c 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -522,6 +522,23 @@ public static bool IsIpv6(string ip) return false; } + public static bool IsIpv4(string? ip) + { + if (ip.IsNullOrEmpty()) + { + return false; + } + + ip = ip.Trim(); + if (!IPAddress.TryParse(ip, out var address)) + { + return false; + } + + return address.AddressFamily == AddressFamily.InterNetwork + && ip.Count(c => c == '.') == 3; + } + public static bool IsIpAddress(string? ip) { if (ip.IsNullOrEmpty()) @@ -847,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) @@ -1097,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) @@ -1197,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 2215a3e3f37..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"; @@ -53,19 +54,23 @@ public static void RegWriteValue(string path, string name, object value) public static async Task RemoveTunDevice() { - try + var tunNameList = new List { "wintunsingbox_tun", "xray_tun" }; + foreach (var tunName in tunNameList) { - var sum = MD5.HashData(Encoding.UTF8.GetBytes("wintunsingbox_tun")); - var guid = new Guid(sum); - var pnpUtilPath = @"C:\Windows\System32\pnputil.exe"; - var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """; + try + { + var sum = MD5.HashData(Encoding.UTF8.GetBytes(tunName)); + var guid = new Guid(sum); + var pnpUtilPath = @"C:\Windows\System32\pnputil.exe"; + var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """; - // Try to remove the device - _ = await Utils.GetCliWrapOutput(pnpUtilPath, arg); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); + // Try to remove the device + _ = await Utils.GetCliWrapOutput(pnpUtilPath, arg); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } } } } 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/Enums/ESpeedActionType.cs b/v2rayN/ServiceLib/Enums/ESpeedActionType.cs index a03aa9df3e2..5a6317345a6 100644 --- a/v2rayN/ServiceLib/Enums/ESpeedActionType.cs +++ b/v2rayN/ServiceLib/Enums/ESpeedActionType.cs @@ -4,6 +4,7 @@ public enum ESpeedActionType { Tcping, Realping, + UdpTest, Speedtest, Mixedtest, FastRealping diff --git a/v2rayN/ServiceLib/Enums/ETransport.cs b/v2rayN/ServiceLib/Enums/ETransport.cs index b1166608553..b315c5f064d 100644 --- a/v2rayN/ServiceLib/Enums/ETransport.cs +++ b/v2rayN/ServiceLib/Enums/ETransport.cs @@ -2,7 +2,7 @@ namespace ServiceLib.Enums; public enum ETransport { - tcp, + raw, kcp, ws, httpupgrade, 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 bf8dd7aba2f..c5386097571 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -24,6 +24,8 @@ public class Global public const string V2raySampleHttpResponseFileName = NamespaceSample + "SampleHttpResponse"; public const string V2raySampleInbound = NamespaceSample + "SampleInbound"; public const string V2raySampleOutbound = NamespaceSample + "SampleOutbound"; + public const string V2raySampleTunInbound = NamespaceSample + "SampleTunInbound"; + public const string V2raySampleTunRules = NamespaceSample + "SampleTunRules"; public const string SingboxSampleOutbound = NamespaceSample + "SingboxSampleOutbound"; public const string CustomRoutingFileName = NamespaceSample + "custom_routing_"; public const string TunSingboxDNSFileName = NamespaceSample + "tun_singbox_dns"; @@ -42,15 +44,18 @@ public class Global public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter"; public const string DefaultSecurity = "auto"; - public const string DefaultNetwork = "tcp"; - public const string TcpHeaderHttp = "http"; + public const string DefaultNetwork = "raw"; + public const string RawHeaderHttp = "http"; public const string None = "none"; + public const string RawNetworkAlias = "tcp"; + public const string DefaultXhttpMode = "auto"; public const string ProxyTag = "proxy"; public const string DirectTag = "direct"; public const string BlockTag = "block"; + 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"; @@ -59,9 +64,12 @@ public class Global public const string HttpsProtocol = "https://"; public const string SocksProtocol = "socks://"; public const string Socks5Protocol = "socks5://"; + public const string InnerUriProtocol = "v2rayn://"; public const string AsIs = "AsIs"; public const string IPIfNonMatch = "IPIfNonMatch"; public const string IPOnDemand = "IPOnDemand"; + public const string GeoSitePrefix = "geosite:"; + public const string GeoIPPrefix = "geoip:"; public const string UserEMail = "t@t.tt"; public const string AutoRunRegPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; @@ -82,6 +90,8 @@ public class Global public const string XrayLocalCert = "XRAY_LOCATION_CERT"; public const int SpeedTestPageSize = 1000; public const string LinuxBash = "/bin/bash"; + public const string StringTrue = "true"; + public const string StringFalse = "false"; public const string SingboxDirectDNSTag = "direct_dns"; public const string SingboxRemoteDNSTag = "remote_dns"; @@ -89,7 +99,7 @@ public class Global public const string SingboxHostsDNSTag = "hosts_dns"; public const string SingboxFakeDNSTag = "fake_dns"; - public const int Hysteria2DefaultHopInt = 10; + public const int Hysteria2DefaultHopInt = 30; public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset"; @@ -141,14 +151,19 @@ 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=100000000", + @"https://speed.cloudflare.com/__down?bytes=99999999", ]; 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" @@ -182,14 +197,15 @@ public class Global @"https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/main/v2rayN/" ]; - public static readonly Dictionary TcpHttpUserAgentTexts = new() + public static readonly Dictionary RawHttpUserAgentTexts = new() { {"chrome","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36" }, {"firefox","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0" }, {"safari","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" }, {"edge","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70" }, {"none",""}, - {"golang",""} + {"golang","Go-http-client/1.1"}, + {"curl","curl/7.68.0"}, }; public const string Hysteria2ProtocolShare = "hy2://"; @@ -198,6 +214,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://" }, @@ -292,14 +312,12 @@ public class Global public static readonly List Networks = [ - "tcp", + "raw", + "xhttp", "kcp", + "grpc", "ws", - "httpupgrade", - "xhttp", - "h2", - "quic", - "grpc" + "httpupgrade" ]; public static readonly List KcpHeaderTypes = @@ -314,12 +332,12 @@ public class Global public static readonly Dictionary KcpHeaderMaskMap = new() { - { "srtp", "header-srtp" }, - { "utp", "header-utp" }, - { "wechat-video", "header-wechat" }, - { "dtls", "header-dtls" }, - { "wireguard", "header-wireguard" }, - { "dns", "header-dns" } + { "srtp", "srtp" }, + { "utp", "utp" }, + { "wechat-video", "wechat" }, + { "dtls", "dtls" }, + { "wireguard", "wireguard" }, + { "dns", "dns" } }; public static readonly List CoreTypes = @@ -393,7 +411,8 @@ public class Global "chrome", "firefox", "edge", - "golang" + "curl", + "golang", ]; public static readonly List XhttpMode = @@ -404,13 +423,6 @@ public class Global "stream-one" ]; - public static readonly List AllowInsecure = - [ - "true", - "false", - "" - ]; - public static readonly List DomainStrategy = [ "AsIs", @@ -424,11 +436,12 @@ public class Global public static readonly List DomainDirectDNSAddress = [ - "https://dns.alidns.com/dns-query", - "https://doh.pub/dns-query", - "https://dns.alidns.com/dns-query,https://doh.pub/dns-query", - "223.5.5.5", "119.29.29.29", + "223.5.5.5", + "119.29.29.29,223.5.5.5,https://doh.pub/dns-query", + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query", + "https://doh.pub/dns-query,https://dns.alidns.com/dns-query", "localhost" ]; @@ -440,7 +453,7 @@ public class Global "https://dns.cloudflare.com/dns-query", "https://doh.dns.sb/dns-query", "https://doh.opendns.com/dns-query", - "https://common.dot.dns.yandex.net", + "https://common.dot.dns.yandex.net/dns-query", "8.8.8.8", "1.1.1.1", "185.222.222.222", @@ -450,8 +463,8 @@ public class Global public static readonly List DomainPureIPDNSAddress = [ - "223.5.5.5", "119.29.29.29", + "223.5.5.5", "localhost" ]; @@ -496,6 +509,7 @@ public class Global public static readonly List InboundTags = [ + "tun", "socks", "socks2", "socks3" @@ -505,6 +519,7 @@ public class Global [ "http", "tls", + "quic", "bittorrent" ]; @@ -522,7 +537,6 @@ public class Global "tls", "quic", "fakedns", - "fakedns+others" ]; public static readonly List TunMtus = @@ -634,6 +648,24 @@ public class Global @"" ]; + public static readonly List UdpTestTargets = + [ + "ntp:pool.ntp.org", + "ntp:time.google.com", + "dns:1.1.1.1", + "dns:8.8.8.8", + "dns:dns.google", + "stun:stun.voztovoice.org", + "stun:stun.cloudflare.com", + "stun:stun.l.google.com:19302", + "mcbe:pms.mc-complex.com", + "mcbe:bedrock.opblocks.com", + "mcbe:opsucht.net", + "mcbe:play.craftersmc.net", + "mcbe:mps.lemoncloud.net", + "mcbe:bedrock.talonmc.net", + ]; + public static readonly List OutboundTags = [ ProxyTag, @@ -648,15 +680,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 = @@ -667,14 +699,6 @@ public class Global "" ]; - public static readonly List EchForceQuerys = - [ - "none", - "half", - "full", - "" - ]; - public static readonly List TunIcmpRoutingPolicies = [ "rule", 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 a8cca654126..b7481e1aaa8 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -23,19 +23,6 @@ public record CoreConfigContextBuilderAllResult( public NodeValidatorResult CombinedValidatorResult => new( [.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []], [.. MainResult.ValidatorResult.Warnings, .. PreSocksResult?.ValidatorResult.Warnings ?? []]); - - /// - /// The main context with TunProtectSsPort/ProxyRelaySsPort and ProtectDomainList merged in - /// from the pre-socks result (if any). Pass this to the core runner. - /// - public CoreConfigContext ResolvedMainContext => PreSocksResult is not null - ? MainResult.Context with - { - TunProtectSsPort = PreSocksResult.Context.TunProtectSsPort, - ProxyRelaySsPort = PreSocksResult.Context.ProxyRelaySsPort, - ProtectDomainList = [.. MainResult.Context.ProtectDomainList ?? [], .. PreSocksResult.Context.ProtectDomainList ?? []], - } - : MainResult.Context; } public class CoreConfigContextBuilder @@ -58,10 +45,10 @@ public static async Task Build(Config config, Pr IsTunEnabled = config.TunModeItem.EnableTun, SimpleDnsItem = config.SimpleDNSItem, ProtectDomainList = [], - TunProtectSsPort = 0, - ProxyRelaySsPort = 0, RawDnsItem = await AppManager.Instance.GetDNSItem(coreType), RoutingItem = await ConfigHandler.GetDefaultRouting(config), + IsWindows = Utils.IsWindows(), + IsMacOS = Utils.IsMacOS(), }; var validatorResult = NodeValidatorResult.Empty(); var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node); @@ -104,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); } @@ -122,7 +123,20 @@ public static async Task BuildAll(Config conf } var preResult = await BuildPreSocksIfNeeded(mainResult.Context); - return new CoreConfigContextBuilderAllResult(mainResult, preResult); + if (preResult is null) + { + return new CoreConfigContextBuilderAllResult(mainResult, null); + } + + var resolvedMainResult = mainResult with + { + Context = mainResult.Context with + { + IsTunEnabled = false, // main core doesn't handle tun directly when pre-socks is used + ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList], + } + }; + return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult); } /// @@ -148,37 +162,7 @@ public static async Task BuildAll(Config conf }; } - if (!nodeContext.IsTunEnabled - || coreType != ECoreType.Xray - || node.ConfigType == EConfigType.Custom) - { - return null; - } - - var tunProtectSsPort = Utils.GetFreePort(); - var proxyRelaySsPort = Utils.GetFreePort(); - var preItem = new ProfileItem() - { - CoreType = ECoreType.sing_box, - ConfigType = EConfigType.Shadowsocks, - Address = Global.Loopback, - Port = proxyRelaySsPort, - Password = Global.None, - }; - preItem.SetProtocolExtra(preItem.GetProtocolExtra() with - { - SsMethod = Global.None, - }); - var preResult2 = await Build(nodeContext.AppConfig, preItem); - return preResult2 with - { - Context = preResult2.Context with - { - ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preResult2.Context.ProtectDomainList ?? []], - TunProtectSsPort = tunProtectSsPort, - ProxyRelaySsPort = proxyRelaySsPort, - } - }; + return null; } /// @@ -309,6 +293,11 @@ private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext con } var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType); + var msgs = new List([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]); + if (msgs.Count > 0) + { + Logging.SaveLog($"{node.Remarks}: {string.Join("; ", msgs)}"); + } if (!nodeValidatorResult.Success) { return nodeValidatorResult; @@ -340,8 +329,9 @@ private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext con } // xhttp downloadSettings address protect - if (!string.IsNullOrEmpty(node.Extra) - && JsonUtils.ParseJson(node.Extra) is JsonObject extra + var xhttpExtra = node.GetTransportExtra().XhttpExtra; + if (!string.IsNullOrEmpty(xhttpExtra) + && JsonUtils.ParseJson(xhttpExtra) is JsonObject extra && extra.TryGetPropertyValue("downloadSettings", out var dsNode) && dsNode is JsonObject downloadSettings && downloadSettings.TryGetPropertyValue("address", out var dAddrNode) diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs index 65110b2ab5b..9665144da38 100644 --- a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -20,7 +20,7 @@ public class NodeValidator [EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks]; private static readonly HashSet SingboxShadowsocksAllowedTransports = - [nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)]; + [nameof(ETransport.raw), nameof(ETransport.ws)]; public static NodeValidatorResult Validate(ProfileItem item, ECoreType coreType) { @@ -29,35 +29,6 @@ public static NodeValidatorResult Validate(ProfileItem item, ECoreType coreType) return v.ToResult(); } - private class ValidationContext - { - public List Errors { get; } = []; - public List Warnings { get; } = []; - - public void Error(string message) - { - Errors.Add(message); - } - - public void Warning(string message) - { - Warnings.Add(message); - } - - public void Assert(bool condition, string errorMsg) - { - if (!condition) - { - Error(errorMsg); - } - } - - public NodeValidatorResult ToResult() - { - return new NodeValidatorResult(Errors, Warnings); - } - } - private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v) { if (item.ConfigType is EConfigType.Custom) @@ -126,6 +97,21 @@ private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreT break; } + if (coreType is ECoreType.Xray + && (protocolExtra.Flow ?? string.Empty).StartsWith("xtls", StringComparison.OrdinalIgnoreCase) + && item.MuxEnabled == true) + { + v.Warning(string.Format(ResUI.MsgOptionsConflict, "XTLS", "Mux.Cool")); + } + + if (item.GetNetwork() is nameof(ETransport.ws) + && item.EchConfigList.IsNullOrEmpty() + && item.GetAlpn()?.FirstOrDefault() == "h3") + { + v.Warning( + "WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues."); + } + // TLS & Security if (item.StreamSecurity == Global.StreamSecurity) { @@ -134,6 +120,25 @@ 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.GetAllowInsecure() + && item.Cert.IsNullOrEmpty() + && item.CertSha.IsNullOrEmpty()) + { + v.Warning(ResUI.MsgAllowInsecureDeprecated); + } + + if ((coreType == ECoreType.Xray + && item.GetAllowInsecure() + && item.Cert.IsNullOrEmpty() + && item.CertSha.IsNullOrEmpty()) + || (coreType == ECoreType.sing_box + && item.GetAllowInsecure() + && item.Cert.IsNullOrEmpty())) + { + v.Warning("Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks."); + } } if (item.StreamSecurity == Global.StreamSecurityReality) @@ -141,13 +146,22 @@ private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreT v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey")); } - if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty()) + var transport = item.GetTransportExtra(); + if (item.Network == nameof(ETransport.xhttp) && !transport.XhttpExtra.IsNullOrEmpty()) { - if (JsonUtils.ParseJson(item.Extra) is null) + if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject) { v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra")); } } + + if (!item.Finalmask.IsNullOrEmpty()) + { + if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject) + { + v.Error(string.Format(ResUI.MsgInvalidProperty, "Finalmask")); + } + } } private static string? ValidateSingboxTransport(EConfigType configType, string net) @@ -159,7 +173,7 @@ private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreT } // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks - if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp)) + if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.raw)) { return string.Format(ResUI.MsgCoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), configType.ToString(), net); @@ -174,4 +188,33 @@ private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreT return null; } + + private class ValidationContext + { + public List Errors { get; } = []; + public List Warnings { get; } = []; + + public void Error(string message) + { + Errors.Add(message); + } + + public void Warning(string message) + { + Warnings.Add(message); + } + + public void Assert(bool condition, string errorMsg) + { + if (!condition) + { + Error(errorMsg); + } + } + + public NodeValidatorResult ToResult() + { + return new NodeValidatorResult(Errors, Warnings); + } + } } diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index c3d820431eb..23286f3095e 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -39,7 +39,6 @@ public static class ConfigHandler { LogEnabled = false, Loglevel = "warning", - MuxEnabled = false, }; if (config.Inbound == null) @@ -76,10 +75,11 @@ public static class ConfigHandler Tti = 50, UplinkCapacity = 12, DownlinkCapacity = 100, - ReadBufferSize = 2, - WriteBufferSize = 2, - Congestion = false + CwndMultiplier = 1, + MaxSendingWindow = 2 * 1024 * 1024, }; + config.KcpItem.CwndMultiplier = config.KcpItem.CwndMultiplier <= 0 ? 1 : config.KcpItem.CwndMultiplier; + config.KcpItem.MaxSendingWindow = config.KcpItem.MaxSendingWindow <= 0 ? (2 * 1024 * 1024) : config.KcpItem.MaxSendingWindow; config.GrpcItem ??= new GrpcItem { IdleTimeout = 60, @@ -133,6 +133,10 @@ public static class ConfigHandler { config.SpeedTestItem.MixedConcurrencyCount = 5; } + if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty()) + { + config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First(); + } config.Mux4RayItem ??= new() { @@ -160,7 +164,7 @@ public static class ConfigHandler config.Fragment4RayItem ??= new() { Packets = "tlshello", - Length = "100-200", + Length = "50-100", Interval = "10-20" }; config.GlobalHotkeys ??= new(); @@ -235,9 +239,6 @@ public static async Task AddServer(Config config, ProfileItem profileItem) item.Password = profileItem.Password; item.Network = profileItem.Network; - item.HeaderType = profileItem.HeaderType; - item.RequestHost = profileItem.RequestHost; - item.Path = profileItem.Path; item.StreamSecurity = profileItem.StreamSecurity; item.Sni = profileItem.Sni; @@ -249,14 +250,14 @@ public static async Task AddServer(Config config, ProfileItem profileItem) item.ShortId = profileItem.ShortId; item.SpiderX = profileItem.SpiderX; item.Mldsa65Verify = profileItem.Mldsa65Verify; - item.Extra = profileItem.Extra; item.MuxEnabled = profileItem.MuxEnabled; item.Cert = profileItem.Cert; item.CertSha = profileItem.CertSha; item.EchConfigList = profileItem.EchConfigList; - item.EchForceQuery = profileItem.EchForceQuery; + item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName; item.Finalmask = profileItem.Finalmask; item.ProtoExtra = profileItem.ProtoExtra; + item.TransportExtra = profileItem.TransportExtra; } var ret = item.ConfigType switch @@ -296,9 +297,6 @@ public static async Task AddVMessServer(Config config, ProfileItem profileI VmessSecurity = profileItem.GetProtocolExtra().VmessSecurity?.TrimEx() }); profileItem.Network = profileItem.Network.TrimEx(); - profileItem.HeaderType = profileItem.HeaderType.TrimEx(); - profileItem.RequestHost = profileItem.RequestHost.TrimEx(); - profileItem.Path = profileItem.Path.TrimEx(); profileItem.StreamSecurity = profileItem.StreamSecurity.TrimEx(); if (!Global.VmessSecurities.Contains(profileItem.GetProtocolExtra().VmessSecurity)) @@ -706,10 +704,12 @@ public static async Task AddTrojanServer(Config config, ProfileItem profile public static async Task AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true) { profileItem.ConfigType = EConfigType.Hysteria2; - //profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); profileItem.Password = profileItem.Password.TrimEx(); + profileItem.Fingerprint = string.Empty; + profileItem.Alpn = string.Empty; + //profileItem.Alpn = "h3"; profileItem.Network = string.Empty; if (profileItem.StreamSecurity.IsNullOrEmpty()) @@ -749,11 +749,14 @@ public static async Task AddTuicServer(Config config, ProfileItem profileIt profileItem.Username = profileItem.Username.TrimEx(); profileItem.Password = profileItem.Password.TrimEx(); profileItem.Network = string.Empty; + profileItem.Fingerprint = string.Empty; - if (!Global.TuicCongestionControls.Contains(profileItem.HeaderType)) + var congestionControl = profileItem.GetProtocolExtra().CongestionControl; + if (!Global.TuicCongestionControls.Contains(congestionControl)) { - profileItem.HeaderType = Global.TuicCongestionControls.FirstOrDefault()!; + congestionControl = Global.TuicCongestionControls.FirstOrDefault()!; } + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with { CongestionControl = congestionControl }); if (profileItem.StreamSecurity.IsNullOrEmpty()) { @@ -787,12 +790,30 @@ public static async Task AddWireguardServer(Config config, ProfileItem prof profileItem.Address = profileItem.Address.TrimEx(); profileItem.Password = profileItem.Password.TrimEx(); + var wgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx(); + if (!wgReserved.IsNullOrEmpty() + && !wgReserved.Contains(',')) + { + // Base64 format, convert to standard format + try + { + var bytes = Convert.FromBase64String(wgReserved); + var reserved = new byte[3]; + Array.Copy(bytes, reserved, Math.Min(bytes.Length, 3)); + + wgReserved = string.Join(", ", reserved); + } + catch + { + // If conversion fails, keep the original value + } + } profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with { WgPublicKey = profileItem.GetProtocolExtra().WgPublicKey?.TrimEx(), WgPresharedKey = profileItem.GetProtocolExtra().WgPresharedKey?.TrimEx(), WgInterfaceAddress = profileItem.GetProtocolExtra().WgInterfaceAddress?.TrimEx(), - WgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx(), + WgReserved = wgReserved, WgMtu = profileItem.GetProtocolExtra().WgMtu is null or <= 0 ? Global.TunMtus.First() : profileItem.GetProtocolExtra().WgMtu, }); @@ -850,8 +871,10 @@ public static async Task AddNaiveServer(Config config, ProfileItem profileI profileItem.Address = profileItem.Address.TrimEx(); profileItem.Username = profileItem.Username.TrimEx(); profileItem.Password = profileItem.Password.TrimEx(); + profileItem.Fingerprint = string.Empty; profileItem.Alpn = string.Empty; profileItem.Network = string.Empty; + profileItem.AllowInsecure = string.Empty; if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; @@ -900,6 +923,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"), @@ -920,6 +944,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(), @@ -940,6 +965,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(), @@ -995,9 +1021,6 @@ public static async Task AddVlessServer(Config config, ProfileItem profileI profileItem.Address = profileItem.Address.TrimEx(); profileItem.Password = profileItem.Password.TrimEx(); profileItem.Network = profileItem.Network.TrimEx(); - profileItem.HeaderType = profileItem.HeaderType.TrimEx(); - profileItem.RequestHost = profileItem.RequestHost.TrimEx(); - profileItem.Path = profileItem.Path.TrimEx(); profileItem.StreamSecurity = profileItem.StreamSecurity.TrimEx(); var vlessEncryption = profileItem.GetProtocolExtra().VlessEncryption?.TrimEx(); @@ -1042,14 +1065,20 @@ public static async Task> DedupServerList(Config config, string foreach (var item in lstProfile) { - if (!lstKeep.Exists(i => CompareProfileItem(i, item, false))) + if (item.IsComplex()) { lstKeep.Add(item); + continue; } - else + + if (lstKeep.Exists(i => CompareProfileItem(i, item, false))) { lstRemove.Add(item); } + else + { + lstKeep.Add(item); + } } await RemoveServers(config, lstRemove); @@ -1066,7 +1095,7 @@ public static async Task> DedupServerList(Config config, string /// 0 if successful public static async Task AddServerCommon(Config config, ProfileItem profileItem, bool toFile = true) { - profileItem.ConfigVersion = 3; + profileItem.ConfigVersion = 4; if (profileItem.StreamSecurity.IsNotEmpty()) { @@ -1077,10 +1106,6 @@ public static async Task AddServerCommon(Config config, ProfileItem profile } else { - if (profileItem.AllowInsecure.IsNullOrEmpty()) - { - profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower(); - } if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality) { profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint; @@ -1134,6 +1159,8 @@ private static bool CompareProfileItem(ProfileItem? o, ProfileItem? n, bool rema var oProtocolExtra = o.GetProtocolExtra(); var nProtocolExtra = n.GetProtocolExtra(); + var oTransport = o.GetTransportExtra(); + var nTransport = n.GetTransportExtra(); return o.ConfigType == n.ConfigType && AreEqual(o.Address, n.Address) @@ -1144,9 +1171,16 @@ private static bool CompareProfileItem(ProfileItem? o, ProfileItem? n, bool rema && AreEqual(oProtocolExtra.SsMethod, nProtocolExtra.SsMethod) && AreEqual(oProtocolExtra.VmessSecurity, nProtocolExtra.VmessSecurity) && AreEqual(o.Network, n.Network) - && AreEqual(o.HeaderType, n.HeaderType) - && AreEqual(o.RequestHost, n.RequestHost) - && AreEqual(o.Path, n.Path) + && AreEqual(oTransport.RawHeaderType, nTransport.RawHeaderType) + && AreEqual(oTransport.Host, nTransport.Host) + && AreEqual(oTransport.Path, nTransport.Path) + && AreEqual(oTransport.XhttpMode, nTransport.XhttpMode) + && AreEqual(oTransport.XhttpExtra, nTransport.XhttpExtra) + && AreEqual(oTransport.GrpcAuthority, nTransport.GrpcAuthority) + && AreEqual(oTransport.GrpcServiceName, nTransport.GrpcServiceName) + && AreEqual(oTransport.GrpcMode, nTransport.GrpcMode) + && AreEqual(oTransport.KcpHeaderType, nTransport.KcpHeaderType) + && AreEqual(oTransport.KcpSeed, nTransport.KcpSeed) && (o.ConfigType == EConfigType.Trojan || o.StreamSecurity == n.StreamSecurity) && AreEqual(oProtocolExtra.Flow, nProtocolExtra.Flow) && AreEqual(oProtocolExtra.SalamanderPass, nProtocolExtra.SalamanderPass) @@ -1416,10 +1450,12 @@ public static async Task AddGroupRegionServer(Config config, SubItem? public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) { ProfileItem? itemSocks = null; + var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect + || Utils.IsNonWindows(); if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun - && config.TunModeItem.EnableLegacyProtect) + && enableLegacyProtect) { itemSocks = new ProfileItem() { @@ -1432,7 +1468,7 @@ public static async Task AddGroupRegionServer(Config config, SubItem? else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort is > 0 and <= 65535) { - var preCoreType = AppManager.Instance.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; + var preCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; itemSocks = new ProfileItem() { CoreType = preCoreType, @@ -1491,10 +1527,8 @@ private static async Task AddBatchServersCommon(Config config, string strDa } var subFilter = string.Empty; - //remove sub items if (isSub && subid.IsNotEmpty()) { - await RemoveServersViaSubid(config, subid, isSub); subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? ""; } @@ -1597,10 +1631,6 @@ private static async Task AddBatchServers4Custom(Config config, string strD } if (lstProfiles != null && lstProfiles.Count > 0) { - if (isSub && subid.IsNotEmpty()) - { - await RemoveServersViaSubid(config, subid, isSub); - } var count = 0; foreach (var it in lstProfiles) { @@ -1620,40 +1650,23 @@ private static async Task AddBatchServers4Custom(Config config, string strD ProfileItem? profileItem = null; //Is sing-box configuration - if (profileItem is null) - { - profileItem = SingboxFmt.ResolveFull(strData, subRemarks); - } + profileItem ??= SingboxFmt.ResolveFull(strData, subRemarks); //Is v2ray configuration - if (profileItem is null) - { - profileItem = V2rayFmt.ResolveFull(strData, subRemarks); - } + profileItem ??= V2rayFmt.ResolveFull(strData, subRemarks); //Is Html Page if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData)) { return -1; } //Is Clash configuration - if (profileItem is null) - { - profileItem = ClashFmt.ResolveFull(strData, subRemarks); - } + profileItem ??= ClashFmt.ResolveFull(strData, subRemarks); //Is hysteria configuration - if (profileItem is null) - { - profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks); - } + profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks); if (profileItem is null || profileItem.Address.IsNullOrEmpty()) { return -1; } - if (isSub && subid.IsNotEmpty()) - { - await RemoveServersViaSubid(config, subid, isSub); - } - profileItem.Subid = subid; profileItem.IsSub = isSub; profileItem.PreSocksPort = preSocksPort; @@ -1683,11 +1696,6 @@ private static async Task AddBatchServers4SsSIP008(Config config, string st return -1; } - if (isSub && subid.IsNotEmpty()) - { - await RemoveServersViaSubid(config, subid, isSub); - } - var lstSsServer = ShadowsocksFmt.ResolveSip008(strData); if (lstSsServer?.Count > 0) { @@ -1708,6 +1716,86 @@ private static async Task AddBatchServers4SsSIP008(Config config, string st return -1; } + private static async Task AddBatchServers4Wireguard(Config config, string strData, string subid, bool isSub) + { + if (strData.IsNullOrEmpty()) + { + return -1; + } + if (!(strData.Contains("[Interface]", StringComparison.OrdinalIgnoreCase) + && strData.Contains("[Peer]", StringComparison.OrdinalIgnoreCase))) + { + return -1; + } + var lstServer = WireguardFmt.ResolveConfig(strData); + if (lstServer?.Count > 0) + { + var counter = 0; + foreach (var item in lstServer) + { + item.Subid = subid; + item.IsSub = isSub; + if (await AddWireguardServer(config, item) == 0) + { + counter++; + } + } + await SaveConfig(config); + return counter; + } + return -1; + } + + private static async Task AddBatchServers4InnerUri(Config config, string strData, string subid, bool isSub) + { + if (strData.IsNullOrEmpty()) + { + return -1; + } + + var lstServer = InnerFmt.Resolve(strData, subid); + if (lstServer?.Count > 0) + { + var counter = 0; + List lstAdd = []; + foreach (var profileItem in lstServer) + { + profileItem.Subid = subid; + profileItem.IsSub = isSub; + + var addStatus = profileItem.ConfigType switch + { + EConfigType.VMess => await AddVMessServer(config, profileItem, false), + EConfigType.Shadowsocks => await AddShadowsocksServer(config, profileItem, false), + EConfigType.HTTP => await AddHttpServer(config, profileItem, false), + EConfigType.SOCKS => await AddSocksServer(config, profileItem, false), + EConfigType.Trojan => await AddTrojanServer(config, profileItem, false), + EConfigType.VLESS => await AddVlessServer(config, profileItem, false), + EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem, false), + EConfigType.TUIC => await AddTuicServer(config, profileItem, false), + EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false), + EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false), + EConfigType.Naive => await AddNaiveServer(config, profileItem, false), + EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false), + _ => -1, + }; + if (addStatus == 0) + { + counter++; + lstAdd.Add(profileItem); + } + } + if (lstAdd.Count > 0) + { + await SQLiteHelper.Instance.InsertAllAsync(lstAdd); + } + await SaveConfig(config); + return counter; + } + + return -1; + } + /// /// Main entry point for adding batch servers from various formats /// Tries different parsing methods to import as many servers as possible @@ -1729,6 +1817,7 @@ public static async Task AddBatchServers(Config config, string strData, str { lstOriSub = await AppManager.Instance.ProfileItems(subid); activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId); + await RemoveServersViaSubid(config, subid, true); } var counter = 0; @@ -1750,6 +1839,38 @@ public static async Task AddBatchServers(Config config, string strData, str counter = await AddBatchServers4SsSIP008(config, strData, subid, isSub); } + //maybe wireguard config + if (counter < 1) + { + counter = await AddBatchServers4Wireguard(config, strData, subid, isSub); + } + + //May be standard uri mixed with internal uri + 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) + { + counter += innerUriCount; + } + else + { + counter = innerUriCount; + } + } + //maybe other sub if (counter < 1) { @@ -1930,6 +2051,12 @@ public static async Task DeleteSubItem(Config config, string id) await SQLiteHelper.Instance.DeleteAsync(item); await RemoveServersViaSubid(config, id, false); + if (item.Id == config.SubIndexId) + { + var subs = await AppManager.Instance.SubItems(); + config.SubIndexId = subs.LastOrDefault()?.Id; + } + return 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 b620cfe909a..1427581ec99 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -6,6 +6,8 @@ public class BaseFmt { private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" }; + private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty); + protected static string GetIpv6(string address) { if (Utils.IsIpv6(address)) @@ -21,6 +23,8 @@ protected static string GetIpv6(string address) protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary dicQuery) { + var transport = item.GetTransportExtra(); + if (item.StreamSecurity.IsNotEmpty()) { dicQuery.Add("security", item.StreamSecurity); @@ -69,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)); @@ -87,54 +95,69 @@ protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dicti dicQuery.Add("fm", Utils.UrlEncode(finalmask)); } - dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); + var network = item.GetNetwork(); + if (!Global.Networks.Contains(network)) + { + network = nameof(ETransport.raw); + } - switch (item.Network) + //dicQuery.Add("type", network); + dicQuery.Add("type", network == nameof(ETransport.raw) ? Global.RawNetworkAlias : network); + + switch (network) { - case nameof(ETransport.tcp): - dicQuery.Add("headerType", item.HeaderType.IsNotEmpty() ? item.HeaderType : Global.None); - if (item.RequestHost.IsNotEmpty()) + case nameof(ETransport.raw): + dicQuery.Add("headerType", transport.RawHeaderType.IsNotEmpty() ? transport.RawHeaderType : Global.None); + if (transport.Host.IsNotEmpty()) + { + dicQuery.Add("host", UrlEncodeSafe(transport.Host)); + } + if (transport.Path.IsNotEmpty()) { - dicQuery.Add("host", Utils.UrlEncode(item.RequestHost)); + dicQuery.Add("path", UrlEncodeSafe(transport.Path)); } break; case nameof(ETransport.kcp): - dicQuery.Add("headerType", item.HeaderType.IsNotEmpty() ? item.HeaderType : Global.None); - if (item.Path.IsNotEmpty()) + dicQuery.Add("headerType", transport.KcpHeaderType.IsNotEmpty() ? transport.KcpHeaderType : Global.None); + if (transport.KcpSeed.IsNotEmpty()) + { + dicQuery.Add("seed", UrlEncodeSafe(transport.KcpSeed)); + } + if (transport.KcpMtu > 0) { - dicQuery.Add("seed", Utils.UrlEncode(item.Path)); + dicQuery.Add("mtu", transport.KcpMtu.ToString()); } break; case nameof(ETransport.ws): case nameof(ETransport.httpupgrade): - if (item.RequestHost.IsNotEmpty()) + if (transport.Host.IsNotEmpty()) { - dicQuery.Add("host", Utils.UrlEncode(item.RequestHost)); + dicQuery.Add("host", UrlEncodeSafe(transport.Host)); } - if (item.Path.IsNotEmpty()) + if (transport.Path.IsNotEmpty()) { - dicQuery.Add("path", Utils.UrlEncode(item.Path)); + dicQuery.Add("path", UrlEncodeSafe(transport.Path)); } break; case nameof(ETransport.xhttp): - if (item.RequestHost.IsNotEmpty()) + if (transport.Host.IsNotEmpty()) { - dicQuery.Add("host", Utils.UrlEncode(item.RequestHost)); + dicQuery.Add("host", UrlEncodeSafe(transport.Host)); } - if (item.Path.IsNotEmpty()) + if (transport.Path.IsNotEmpty()) { - dicQuery.Add("path", Utils.UrlEncode(item.Path)); + dicQuery.Add("path", UrlEncodeSafe(transport.Path)); } - if (item.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(item.HeaderType)) + if (transport.XhttpMode.IsNotEmpty() && Global.XhttpMode.Contains(transport.XhttpMode)) { - dicQuery.Add("mode", Utils.UrlEncode(item.HeaderType)); + dicQuery.Add("mode", UrlEncodeSafe(transport.XhttpMode)); } - if (item.Extra.IsNotEmpty()) + if (transport.XhttpExtra.IsNotEmpty()) { - var node = JsonUtils.ParseJson(item.Extra); + var node = JsonUtils.ParseJson(transport.XhttpExtra); var extra = node != null ? JsonUtils.Serialize(node, new JsonSerializerOptions { @@ -142,38 +165,19 @@ protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dicti DefaultIgnoreCondition = JsonIgnoreCondition.Never, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) - : item.Extra; - dicQuery.Add("extra", Utils.UrlEncode(extra)); - } - break; - - case nameof(ETransport.http): - case nameof(ETransport.h2): - dicQuery["type"] = nameof(ETransport.http); - if (item.RequestHost.IsNotEmpty()) - { - dicQuery.Add("host", Utils.UrlEncode(item.RequestHost)); - } - if (item.Path.IsNotEmpty()) - { - dicQuery.Add("path", Utils.UrlEncode(item.Path)); + : transport.XhttpExtra; + dicQuery.Add("extra", UrlEncodeSafe(extra)); } break; - case nameof(ETransport.quic): - dicQuery.Add("headerType", item.HeaderType.IsNotEmpty() ? item.HeaderType : Global.None); - dicQuery.Add("quicSecurity", Utils.UrlEncode(item.RequestHost)); - dicQuery.Add("key", Utils.UrlEncode(item.Path)); - break; - case nameof(ETransport.grpc): - if (item.Path.IsNotEmpty()) + if (transport.GrpcServiceName.IsNotEmpty()) { - dicQuery.Add("authority", Utils.UrlEncode(item.RequestHost)); - dicQuery.Add("serviceName", Utils.UrlEncode(item.Path)); - if (item.HeaderType is Global.GrpcGunMode or Global.GrpcMultiMode) + dicQuery.Add("authority", UrlEncodeSafe(transport.GrpcAuthority)); + dicQuery.Add("serviceName", UrlEncodeSafe(transport.GrpcServiceName)); + if (transport.GrpcMode is Global.GrpcGunMode or Global.GrpcMultiMode) { - dicQuery.Add("mode", Utils.UrlEncode(item.HeaderType)); + dicQuery.Add("mode", UrlEncodeSafe(transport.GrpcMode)); } } break; @@ -199,7 +203,7 @@ protected static int ToUriQueryLite(ProfileItem item, ref Dictionary dicQuery) { - if (item.AllowInsecure.Equals(Global.AllowInsecure.First())) + if (item.GetAllowInsecure()) { // Add two for compatibility dicQuery.Add("insecure", "1"); @@ -216,6 +220,8 @@ private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary GetQueryDecoded(query, k) == "1")) { - item.AllowInsecure = Global.AllowInsecure.First(); + item.AllowInsecure = Global.StringTrue; } else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0")) { - item.AllowInsecure = Global.AllowInsecure.Skip(1).First(); + item.AllowInsecure = Global.StringFalse; } else { item.AllowInsecure = string.Empty; } - item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp)); + var net = GetQueryValue(query, "type", nameof(ETransport.raw)); + if (net == Global.RawNetworkAlias) + { + net = nameof(ETransport.raw); + } + if (!Global.Networks.Contains(net)) + { + net = nameof(ETransport.raw); + } + + item.Network = net; switch (item.Network) { - case nameof(ETransport.tcp): - item.HeaderType = GetQueryValue(query, "headerType", Global.None); - item.RequestHost = GetQueryDecoded(query, "host"); + case nameof(ETransport.raw): + transport = transport with + { + RawHeaderType = GetQueryValue(query, "headerType", Global.None), + Host = GetQueryDecoded(query, "host"), + Path = GetQueryDecoded(query, "path"), + }; break; case nameof(ETransport.kcp): - item.HeaderType = GetQueryValue(query, "headerType", Global.None); - item.Path = GetQueryDecoded(query, "seed"); + var kcpSeed = GetQueryDecoded(query, "seed"); + var kcpMtuStr = GetQueryValue(query, "mtu"); + var kcpMtu = int.TryParse(kcpMtuStr, out var mtu) ? mtu : 0; + transport = transport with + { + KcpHeaderType = GetQueryValue(query, "headerType", Global.None), + KcpSeed = kcpSeed, + KcpMtu = kcpMtu > 0 ? mtu : null, + }; break; case nameof(ETransport.ws): case nameof(ETransport.httpupgrade): - item.RequestHost = GetQueryDecoded(query, "host"); - item.Path = GetQueryDecoded(query, "path", "/"); + transport = transport with + { + Host = GetQueryDecoded(query, "host"), + Path = GetQueryDecoded(query, "path", "/"), + }; break; case nameof(ETransport.xhttp): - item.RequestHost = GetQueryDecoded(query, "host"); - item.Path = GetQueryDecoded(query, "path", "/"); - item.HeaderType = GetQueryDecoded(query, "mode"); - var extraDecoded = GetQueryDecoded(query, "extra"); - if (extraDecoded.IsNotEmpty()) + var xhttpExtra = GetQueryDecoded(query, "extra"); + if (xhttpExtra.IsNotEmpty()) { - var node = JsonUtils.ParseJson(extraDecoded); + var node = JsonUtils.ParseJson(xhttpExtra); if (node != null) { - extraDecoded = JsonUtils.Serialize(node, new JsonSerializerOptions + xhttpExtra = JsonUtils.Serialize(node, new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never, @@ -295,31 +323,32 @@ protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem }); } } - item.Extra = extraDecoded; - break; - - case nameof(ETransport.http): - case nameof(ETransport.h2): - item.Network = nameof(ETransport.h2); - item.RequestHost = GetQueryDecoded(query, "host"); - item.Path = GetQueryDecoded(query, "path", "/"); - break; - case nameof(ETransport.quic): - item.HeaderType = GetQueryValue(query, "headerType", Global.None); - item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None); - item.Path = GetQueryDecoded(query, "key"); + transport = transport with + { + Host = GetQueryDecoded(query, "host"), + Path = GetQueryDecoded(query, "path", "/"), + XhttpMode = GetQueryDecoded(query, "mode"), + XhttpExtra = xhttpExtra, + }; break; case nameof(ETransport.grpc): - item.RequestHost = GetQueryDecoded(query, "authority"); - item.Path = GetQueryDecoded(query, "serviceName"); - item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode); + transport = transport with + { + GrpcAuthority = GetQueryDecoded(query, "authority"), + GrpcServiceName = GetQueryDecoded(query, "serviceName"), + GrpcMode = GetQueryDecoded(query, "mode", Global.GrpcGunMode), + }; break; default: + item.Network = nameof(ETransport.raw); break; } + + item.SetTransportExtra(transport); + return 0; } 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 new file mode 100644 index 00000000000..c9ad5844d46 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs @@ -0,0 +1,302 @@ +namespace ServiceLib.Handler.Fmt; + +public class InnerFmt +{ + private static readonly Lazy SessionSalt = new(() => Utils.GetGuid(false)); + + public static List? Resolve(string strData, string subid) + { + var list = new List(); + // Overwrite externally imported indexIds to avoid possible sources of attacks + var indexIdMap = new Dictionary(); + using (var reader = new StringReader(strData)) + { + while (reader.ReadLine() is { } line) + { + if (line.IsNullOrEmpty()) + { + continue; + } + var trimmedLine = line.Trim(); + if (!trimmedLine.StartsWith(Global.InnerUriProtocol, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + var profileItem = ResolveSingle(trimmedLine); + if (profileItem is null) + { + continue; + } + if (profileItem.ConfigType == EConfigType.Custom) + { + // Unsupported, also to avoid possible sources of attacks, skip it + continue; + } + // overwrite indexId + var newIndexId = Utils.GetGuid(false); + if (!profileItem.IndexId.IsNullOrEmpty()) + { + // Ignore duplicated indexId + indexIdMap[profileItem.IndexId] = newIndexId; + } + profileItem.IndexId = newIndexId; + list.Add(profileItem); + } + } + // For group-type profile items, also overwrite the ChildItems and ChildSubId + var emptyGroupProfileList = new List(); + foreach (var item in list.Where(i => i.ConfigType.IsGroupType())) + { + var protocolExtra = item.GetProtocolExtra(); + // Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks, + // which means it will be replaced with the subid, otherwise set it to null + //if (!protocolExtra.SubChildItems.IsNullOrEmpty()) + if (protocolExtra.SubChildItems == "self") + { + protocolExtra = protocolExtra with + { + SubChildItems = subid + }; + } + else + { + protocolExtra = protocolExtra with + { + SubChildItems = null + }; + } + if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds) + { + var newChildIndexIds = childIndexIds + .Select(id => indexIdMap.GetValueOrDefault(id, null)) + .Where(id => !id.IsNullOrEmpty()) + .ToList(); + protocolExtra = protocolExtra with + { + ChildItems = Utils.List2String(newChildIndexIds) + }; + } + else + { + protocolExtra = protocolExtra with + { + ChildItems = null + }; + } + item.SetProtocolExtra(protocolExtra); + if (protocolExtra.SubChildItems.IsNullOrEmpty() + && protocolExtra.ChildItems.IsNullOrEmpty()) + { + emptyGroupProfileList.Add(item); + } + } + // Remove empty group profile items + list.RemoveAll(emptyGroupProfileList.Contains); + return list; + } + + public static string? ToUri(List items) + { + var sb = new StringBuilder(); + foreach (var item in items) + { + if (item.ConfigType == EConfigType.Custom) + { + continue; + } + var itemClone = JsonUtils.DeepCopy(item); + if (itemClone is null) + { + continue; + } + // overwrite indexId + var originalIndexId = itemClone.IndexId; + var newIndexId = GetReproducibleExportId(originalIndexId); + itemClone.IndexId = newIndexId; + if (itemClone.ConfigType.IsGroupType()) + { + var protocolExtra = itemClone.GetProtocolExtra(); + if (!protocolExtra.SubChildItems.IsNullOrEmpty()) + { + protocolExtra = protocolExtra with + { + SubChildItems = "self" + }; + } + if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds) + { + var newChildIndexIds = childIndexIds + .Select(GetReproducibleExportId) + .Where(id => !id.IsNullOrEmpty()) + .ToList(); + protocolExtra = protocolExtra with + { + ChildItems = Utils.List2String(newChildIndexIds) + }; + } + itemClone.SetProtocolExtra(protocolExtra); + } + var uri = ToUriSingle(itemClone); + if (!uri.IsNullOrEmpty()) + { + sb.AppendLine(uri); + } + } + return sb.Length > 0 ? sb.ToString() : null; + } + + private static ProfileItem? ResolveSingle(string str) + { + // format: v2rayn://vless/{url-safe base64 encoded_string} + var parsedUri = Utils.TryUri(str); + if (parsedUri is null) + { + return null; + } + var segment = parsedUri.AbsolutePath.TrimStart('/'); + var decodedResult = Utils.Base64Decode(segment); + var jsonNode = JsonUtils.ParseJson(decodedResult); + if (jsonNode is not JsonObject jsonObj) + { + return null; + } + // flatten + // move jsonObj.ProtoExtraObj to jsonObj.ProtoExtra (string) + // move jsonObj.TransportExtraObj to jsonObj.TransportExtra (string) + if (jsonObj.TryGetPropertyValue("ProtoExtraObj", out var protoExtraNode) + && protoExtraNode is JsonObject protoExtraObj) + { + jsonObj["ProtoExtra"] = JsonUtils.Serialize(protoExtraObj, false); + jsonObj.Remove("ProtoExtraObj"); + } + if (jsonObj.TryGetPropertyValue("TransportExtraObj", out var transportExtraNode) + && transportExtraNode is JsonObject transportExtraObj) + { + jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false); + jsonObj.Remove("TransportExtraObj"); + } + var profileItem = JsonUtils.Deserialize(JsonUtils.Serialize(jsonObj, false)); + if (profileItem is null) + { + return null; + } + if (profileItem.ConfigVersion != 4) + { + return null; + } + // Check Enum.IsDefined + if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType)) + { + return null; + } + if (profileItem.CoreType is not (null or ECoreType.Xray or ECoreType.sing_box)) + { + return null; + } + var protocolExtra = profileItem.GetProtocolExtra(); + var multipleLoad = protocolExtra.MultipleLoad; + if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad)) + { + return null; + } + return profileItem; + } + + private static string? ToUriSingle(ProfileItem item) + { + var jsonNode = JsonUtils.ParseJson(JsonUtils.Serialize(item, false)); + if (jsonNode is not JsonObject jsonObj) + { + return null; + } + // unflatten + // move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj + // move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj + if (jsonObj.TryGetPropertyValue("ProtoExtra", out var protoExtraNode) + && protoExtraNode is JsonValue protoExtraValue + && protoExtraValue.TryGetValue(out var protoExtraStr) + && !protoExtraStr.IsNullOrEmpty() + && JsonUtils.ParseJson(protoExtraStr) is JsonObject protoExtraObj) + { + jsonObj["ProtoExtraObj"] = protoExtraObj; + jsonObj.Remove("ProtoExtra"); + } + if (jsonObj.TryGetPropertyValue("TransportExtra", out var transportExtraNode) + && transportExtraNode is JsonValue transportExtraValue + && transportExtraValue.TryGetValue(out var transportExtraStr) + && !transportExtraStr.IsNullOrEmpty() + && JsonUtils.ParseJson(transportExtraStr) is JsonObject transportExtraObj) + { + 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); + var encodedStr = Utils.Base64Encode(jsonStr).Replace('+', '-').Replace('/', '_').Replace("=", ""); + return $"{Global.InnerUriProtocol}{item.ConfigType.ToString().ToLower()}/{encodedStr}"; + } + + private static string GetReproducibleExportId(string originalIndexId) + { + if (originalIndexId.IsNullOrEmpty()) + { + return originalIndexId; + } + + var hash = HashCode.Combine(SessionSalt.Value, originalIndexId) & 0x7FFFFFFF; + var bytes = BitConverter.GetBytes(hash); + return Convert.ToBase64String(bytes).Replace("=", ""); + } + + private static void RemoveEmptyJson(JsonNode? node) + { + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (node is JsonObject jsonObject) + { + var propertiesToRemove = new List(); + + foreach (var property in jsonObject) + { + RemoveEmptyJson(property.Value); + + if (IsEmpty(property.Value)) + { + propertiesToRemove.Add(property.Key); + } + } + + foreach (var key in propertiesToRemove) + { + jsonObject.Remove(key); + } + } + else if (node is JsonArray jsonArray) + { + for (var i = jsonArray.Count - 1; i >= 0; i--) + { + RemoveEmptyJson(jsonArray[i]); + + if (IsEmpty(jsonArray[i])) + { + jsonArray.RemoveAt(i); + } + } + } + } + + private static bool IsEmpty(JsonNode? node) + { + return node switch + { + null => true, + JsonValue value when value.TryGetValue(out var str) => string.IsNullOrEmpty(str), + JsonObject obj => obj.Count == 0, + JsonArray arr => arr.Count == 0, + _ => false + }; + } +} diff --git a/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs index 5b30fa46209..ab805002f78 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs @@ -42,31 +42,28 @@ public class ShadowsocksFmt : BaseFmt //url = Utile.Base64Encode(url); //new Sip002 var pw = Utils.Base64Encode($"{item.GetProtocolExtra().SsMethod}:{item.Password}", true); + var transport = item.GetTransportExtra(); // plugin var plugin = string.Empty; var pluginArgs = string.Empty; - if (item.Network == nameof(ETransport.tcp) && item.HeaderType == Global.TcpHeaderHttp) + if (item.Network == nameof(ETransport.raw) && transport.RawHeaderType == Global.RawHeaderHttp) { plugin = "obfs-local"; - pluginArgs = $"obfs=http;obfs-host={item.RequestHost};"; + pluginArgs = $"obfs=http;obfs-host={transport.Host};"; } else { if (item.Network == nameof(ETransport.ws)) { pluginArgs += "mode=websocket;"; - pluginArgs += $"host={item.RequestHost};"; + pluginArgs += $"host={transport.Host};"; // https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172 // Equal signs and commas [and backslashes] must be escaped with a backslash. - var path = item.Path.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,"); + var path = (transport.Path ?? string.Empty).Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,"); pluginArgs += $"path={path};"; } - else if (item.Network == nameof(ETransport.quic)) - { - pluginArgs += "mode=quic;"; - } if (item.StreamSecurity == Global.StreamSecurity) { pluginArgs += "tls;"; @@ -213,8 +210,11 @@ public class ShadowsocksFmt : BaseFmt { obfsHost = obfsHost.Replace("obfs-host=", ""); item.Network = Global.DefaultNetwork; - item.HeaderType = Global.TcpHeaderHttp; - item.RequestHost = obfsHost; + item.SetTransportExtra(item.GetTransportExtra() with + { + RawHeaderType = Global.RawHeaderHttp, + Host = obfsHost, + }); } } // Parse v2ray-plugin @@ -231,21 +231,20 @@ public class ShadowsocksFmt : BaseFmt if (modeValue == "websocket") { item.Network = nameof(ETransport.ws); + var t = item.GetTransportExtra(); if (!host.IsNullOrEmpty()) { - item.RequestHost = host.Replace("host=", ""); - item.Sni = item.RequestHost; + var wsHost = host.Replace("host=", ""); + t = t with { Host = wsHost }; + item.Sni = wsHost; } if (!path.IsNullOrEmpty()) { var pathValue = path.Replace("path=", ""); pathValue = pathValue.Replace("\\=", "=").Replace("\\,", ",").Replace("\\\\", "\\"); - item.Path = pathValue; + t = t with { Path = pathValue }; } - } - else if (modeValue == "quic") - { - item.Network = nameof(ETransport.quic); + item.SetTransportExtra(t); } if (hasTls) 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/Fmt/VmessFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs index b8760a40161..95e1e152b78 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs @@ -26,6 +26,7 @@ public class VmessFmt : BaseFmt var vmessQRCode = new VmessQRCode { + // vmess link keeps shared transport keys; map from new transport model on export. v = 2, ps = item.Remarks.TrimEx(), add = item.Address, @@ -33,15 +34,39 @@ public class VmessFmt : BaseFmt id = item.Password, aid = int.TryParse(item.GetProtocolExtra()?.AlterId, out var result) ? result : 0, scy = item.GetProtocolExtra().VmessSecurity ?? "", - net = item.Network, - type = item.HeaderType, - host = item.RequestHost, - path = item.Path, + net = item.GetNetwork() == nameof(ETransport.raw) ? Global.RawNetworkAlias : item.Network, + type = item.GetNetwork() switch + { + nameof(ETransport.raw) => item.GetTransportExtra().RawHeaderType, + nameof(ETransport.kcp) => item.GetTransportExtra().KcpHeaderType, + nameof(ETransport.xhttp) => item.GetTransportExtra().XhttpMode, + nameof(ETransport.grpc) => item.GetTransportExtra().GrpcMode, + _ => Global.None, + }, + host = item.GetNetwork() switch + { + nameof(ETransport.raw) => item.GetTransportExtra().Host, + nameof(ETransport.ws) => item.GetTransportExtra().Host, + nameof(ETransport.httpupgrade) => item.GetTransportExtra().Host, + nameof(ETransport.xhttp) => item.GetTransportExtra().Host, + nameof(ETransport.grpc) => item.GetTransportExtra().GrpcAuthority, + _ => null, + }, + path = item.GetNetwork() switch + { + nameof(ETransport.raw) => item.GetTransportExtra().Path, + nameof(ETransport.kcp) => item.GetTransportExtra().KcpSeed, + nameof(ETransport.ws) => item.GetTransportExtra().Path, + nameof(ETransport.httpupgrade) => item.GetTransportExtra().Path, + nameof(ETransport.xhttp) => item.GetTransportExtra().Path, + nameof(ETransport.grpc) => item.GetTransportExtra().GrpcServiceName, + _ => null, + }, tls = item.StreamSecurity, sni = item.Sni, alpn = item.Alpn, fp = item.Fingerprint, - insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0" + insecure = item.GetAllowInsecure() ? "1" : "0" }; var url = JsonUtils.Serialize(vmessQRCode); @@ -70,7 +95,10 @@ public class VmessFmt : BaseFmt } item.Network = Global.DefaultNetwork; - item.HeaderType = Global.None; + var transport = new TransportExtraItem + { + RawHeaderType = Global.None, + }; //item.ConfigVersion = vmessQRCode.v; item.Remarks = Utils.ToString(vmessQRCode.ps); @@ -84,20 +112,35 @@ public class VmessFmt : BaseFmt }); if (vmessQRCode.net.IsNotEmpty()) { - item.Network = vmessQRCode.net; + item.Network = vmessQRCode.net == Global.RawNetworkAlias ? nameof(ETransport.raw) : vmessQRCode.net; } if (vmessQRCode.type.IsNotEmpty()) { - item.HeaderType = vmessQRCode.type; + transport = item.GetNetwork() switch + { + nameof(ETransport.raw) => transport with { RawHeaderType = vmessQRCode.type }, + nameof(ETransport.kcp) => transport with { KcpHeaderType = vmessQRCode.type }, + nameof(ETransport.xhttp) => transport with { XhttpMode = vmessQRCode.type }, + nameof(ETransport.grpc) => transport with { GrpcMode = vmessQRCode.type }, + _ => transport, + }; } - - item.RequestHost = Utils.ToString(vmessQRCode.host); - item.Path = Utils.ToString(vmessQRCode.path); + transport = item.GetNetwork() switch + { + nameof(ETransport.raw) => transport with { Host = Utils.ToString(vmessQRCode.host), Path = Utils.ToString(vmessQRCode.path) }, + nameof(ETransport.kcp) => transport with { KcpSeed = Utils.ToString(vmessQRCode.path) }, + nameof(ETransport.ws) => transport with { Host = Utils.ToString(vmessQRCode.host), Path = Utils.ToString(vmessQRCode.path) }, + nameof(ETransport.httpupgrade) => transport with { Host = Utils.ToString(vmessQRCode.host), Path = Utils.ToString(vmessQRCode.path) }, + nameof(ETransport.xhttp) => transport with { Host = Utils.ToString(vmessQRCode.host), Path = Utils.ToString(vmessQRCode.path) }, + nameof(ETransport.grpc) => transport with { GrpcAuthority = Utils.ToString(vmessQRCode.host), GrpcServiceName = Utils.ToString(vmessQRCode.path) }, + _ => transport, + }; + item.SetTransportExtra(transport); item.StreamSecurity = Utils.ToString(vmessQRCode.tls); item.Sni = Utils.ToString(vmessQRCode.sni); item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Fingerprint = Utils.ToString(vmessQRCode.fp); - item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty; + item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.StringTrue : string.Empty; return item; } @@ -122,7 +165,7 @@ public class VmessFmt : BaseFmt item.SetProtocolExtra(new ProtocolExtraItem { - VmessSecurity = "auto", + VmessSecurity = Global.DefaultSecurity, }); var query = Utils.ParseQueryString(url.Query); diff --git a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs index 73cb6a85c78..b0de10683b1 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs @@ -27,9 +27,10 @@ public class WireguardFmt : BaseFmt item.SetProtocolExtra(item.GetProtocolExtra() with { WgPublicKey = GetQueryDecoded(query, "publickey"), + WgPresharedKey = GetQueryDecoded(query, "presharedkey"), WgReserved = GetQueryDecoded(query, "reserved"), WgInterfaceAddress = GetQueryDecoded(query, "address"), - WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : 1280, + WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : null, }); return item; @@ -48,20 +49,183 @@ public class WireguardFmt : BaseFmt remark = "#" + Utils.UrlEncode(item.Remarks); } + var protoExtra = item.GetProtocolExtra(); var dicQuery = new Dictionary(); - if (!item.GetProtocolExtra().WgPublicKey.IsNullOrEmpty()) + if (!protoExtra.WgPublicKey.IsNullOrEmpty()) { - dicQuery.Add("publickey", Utils.UrlEncode(item.GetProtocolExtra().WgPublicKey)); + dicQuery.Add("publickey", Utils.UrlEncode(protoExtra.WgPublicKey)); } - if (!item.GetProtocolExtra().WgReserved.IsNullOrEmpty()) + if (!protoExtra.WgPresharedKey.IsNullOrEmpty()) { - dicQuery.Add("reserved", Utils.UrlEncode(item.GetProtocolExtra().WgReserved)); + dicQuery.Add("presharedkey", Utils.UrlEncode(protoExtra.WgPresharedKey)); } - if (!item.GetProtocolExtra().WgInterfaceAddress.IsNullOrEmpty()) + if (!protoExtra.WgReserved.IsNullOrEmpty()) { - dicQuery.Add("address", Utils.UrlEncode(item.GetProtocolExtra().WgInterfaceAddress)); + dicQuery.Add("reserved", Utils.UrlEncode(protoExtra.WgReserved)); + } + if (!protoExtra.WgInterfaceAddress.IsNullOrEmpty()) + { + dicQuery.Add("address", Utils.UrlEncode(protoExtra.WgInterfaceAddress)); + } + if (protoExtra.WgMtu > 0) + { + dicQuery.Add("mtu", protoExtra.WgMtu.ToString()); } - dicQuery.Add("mtu", Utils.UrlEncode(item.GetProtocolExtra().WgMtu > 0 ? item.GetProtocolExtra().WgMtu.ToString() : "1280")); return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark); } + + public static List? ResolveConfig(string strData) + { + var interfaceDic = new Dictionary(StringComparer.OrdinalIgnoreCase); + var peerDicList = new List>(); + var currentDicRef = interfaceDic; + using (var reader = new StringReader(strData)) + { + while (reader.ReadLine() is { } line) + { + if (line.IsNullOrEmpty()) + { + continue; + } + + var trimmedLine = line.Trim(); + + if (trimmedLine.Equals("[Interface]", StringComparison.OrdinalIgnoreCase)) + { + currentDicRef = interfaceDic; + continue; + } + if (trimmedLine.Equals("[Peer]", StringComparison.OrdinalIgnoreCase)) + { + var peerDic = new Dictionary(StringComparer.OrdinalIgnoreCase); + peerDicList.Add(peerDic); + currentDicRef = peerDic; + continue; + } + + if (trimmedLine.StartsWith('[') || trimmedLine.StartsWith('#') || trimmedLine.StartsWith(';')) + { + continue; + } + + var idx = line.IndexOf('='); + if (idx <= 0) + { + continue; + } + + var key = line[..idx].Trim(); + var value = line[(idx + 1)..].Trim(); + var commentPos = value.IndexOfAny(['#', ';']); + if (commentPos >= 0) + { + value = value[..commentPos].TrimEnd(); + } + + currentDicRef[key] = value; + } + } + + if (!interfaceDic.TryGetValue("PrivateKey", out var privateKey) || privateKey.IsNullOrEmpty()) + { + return null; + } + + var wgMtu = interfaceDic.TryGetValue("MTU", out var mtuStr) && int.TryParse(mtuStr, out var mtuVal) ? mtuVal : 0; + var wgInterfaceAddress = interfaceDic.TryGetValue("Address", out var interfaceAddress) ? interfaceAddress : string.Empty; + + var index = 0; + var resultList = new List(); + + foreach (var peerDic in peerDicList) + { + if (!peerDic.TryGetValue("Endpoint", out var endpoint) || endpoint.IsNullOrEmpty()) + { + continue; + } + + if (!TryParseEndpoint(endpoint, out var peerAddress, out var peerPort)) + { + continue; + } + + var protoExtra = new ProtocolExtraItem + { + WgPublicKey = (peerDic.TryGetValue("PublicKey", out var publicKey) ? publicKey : string.Empty).NullIfEmpty(), + WgPresharedKey = (peerDic.TryGetValue("PresharedKey", out var presharedKey) ? presharedKey : string.Empty).NullIfEmpty(), + WgInterfaceAddress = wgInterfaceAddress, + WgReserved = (peerDic.TryGetValue("Reserved", out var reserved) ? reserved : string.Empty).NullIfEmpty(), + WgMtu = wgMtu > 0 ? wgMtu : null, + }; + + var item = new ProfileItem + { + Remarks = $"{nameof(EConfigType.WireGuard)} Peer {index + 1}", + ConfigType = EConfigType.WireGuard, + Address = peerAddress, + Port = peerPort, + Password = privateKey, + }; + item.SetProtocolExtra(protoExtra); + resultList.Add(item); + + index += 1; + } + + return resultList; + } + + private static bool TryParseEndpoint(string endpoint, out string address, out int port) + { + address = string.Empty; + port = 2408; + + var trimmedEndpoint = endpoint.Trim(); + if (trimmedEndpoint.IsNullOrEmpty()) + { + return false; + } + + if (trimmedEndpoint[0] == '[') + { + var closeIndex = trimmedEndpoint.IndexOf(']'); + if (closeIndex <= 1) + { + return false; + } + + address = trimmedEndpoint[1..closeIndex].Trim(); + var portIndex = closeIndex + 1; + if (portIndex < trimmedEndpoint.Length && trimmedEndpoint[portIndex] == ':' && + int.TryParse(trimmedEndpoint[(portIndex + 1)..].Trim(), out var bracketedPort) && bracketedPort is > 0 and <= 65535) + { + port = bracketedPort; + } + + return address.IsNotEmpty(); + } + + var lastColonIndex = trimmedEndpoint.LastIndexOf(':'); + if (lastColonIndex <= 0) + { + address = trimmedEndpoint; + return true; + } + + address = trimmedEndpoint[..lastColonIndex].Trim(); + var portText = trimmedEndpoint[(lastColonIndex + 1)..].Trim(); + if (address.IsNullOrEmpty()) + { + return false; + } + + if (int.TryParse(portText, out var parsedPortValue) && parsedPortValue is > 0 and <= 65535) + { + port = parsedPortValue; + return true; + } + + address = trimmedEndpoint; + return true; + } } 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 35847d71e4e..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 @@ -306,10 +313,18 @@ public async Task> GetProfileItemsOrderedByIndexIds(IEnumerabl } #pragma warning disable CS0618 + public async Task MigrateProfileExtra() { - await MigrateProfileExtraGroup(); + await MigrateProfileExtraGroupV2ToV3(); + + await MigrateProfileExtraV2ToV3(); + await MigrateProfileTransportV3ToV4(); + } + + private async Task MigrateProfileExtraV2ToV3() + { const int pageSize = 100; var offset = 0; @@ -325,7 +340,7 @@ public async Task MigrateProfileExtra() break; } - var batchSuccessCount = await MigrateProfileExtraSub(batch); + var batchSuccessCount = await MigrateProfileExtraV2ToV3Sub(batch); // Only increment offset by the number of failed items that remain in the result set // Successfully updated items are automatically excluded from future queries due to ConfigVersion = 3 @@ -335,7 +350,120 @@ public async Task MigrateProfileExtra() //await ProfileGroupItemManager.Instance.ClearAll(); } - private async Task MigrateProfileExtraSub(List batch) + private async Task MigrateProfileTransportV3ToV4() + { + const int pageSize = 100; + var offset = 0; + + while (true) + { + var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion = 3 LIMIT {pageSize} OFFSET {offset}"; + var batch = await SQLiteHelper.Instance.QueryAsync(sql); + if (batch is null || batch.Count == 0) + { + break; + } + + var updateProfileItems = new List(); + foreach (var item in batch) + { + try + { + if (item.Network == Global.RawNetworkAlias) + { + item.Network = nameof(ETransport.raw); + } + var transport = item.GetTransportExtra(); + var network = item.GetNetwork(); + + switch (network) + { + case nameof(ETransport.raw): + transport = transport with + { + RawHeaderType = item.HeaderType.NullIfEmpty(), + Host = item.RequestHost.NullIfEmpty(), + Path = item.Path.NullIfEmpty(), + }; + break; + + case nameof(ETransport.ws): + case nameof(ETransport.httpupgrade): + transport = transport with + { + Host = item.RequestHost.NullIfEmpty(), + Path = item.Path.NullIfEmpty(), + }; + break; + + case nameof(ETransport.xhttp): + transport = transport with + { + Host = item.RequestHost.NullIfEmpty(), + Path = item.Path.NullIfEmpty(), + XhttpMode = item.HeaderType.NullIfEmpty(), + XhttpExtra = item.Extra.NullIfEmpty(), + }; + break; + + case nameof(ETransport.grpc): + transport = transport with + { + GrpcAuthority = item.RequestHost.NullIfEmpty(), + GrpcServiceName = item.Path.NullIfEmpty(), + GrpcMode = item.HeaderType.NullIfEmpty(), + }; + break; + + case nameof(ETransport.kcp): + transport = transport with + { + KcpHeaderType = item.HeaderType.NullIfEmpty(), + KcpSeed = item.Path.NullIfEmpty(), + }; + break; + + default: + item.Network = Global.DefaultNetwork; + transport = transport with + { + RawHeaderType = item.HeaderType.NullIfEmpty(), + Host = item.RequestHost.NullIfEmpty(), + }; + break; + } + + item.SetTransportExtra(transport); + item.ConfigVersion = 4; + updateProfileItems.Add(item); + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileTransportV3ToV4 Error: {ex}"); + } + } + + if (updateProfileItems.Count > 0) + { + try + { + var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems); + offset += batch.Count - count; + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileTransportV3ToV4 update error: {ex}"); + offset += batch.Count; + } + } + else + { + offset += batch.Count; + } + } + } + + private async Task MigrateProfileExtraV2ToV3Sub(List batch) { var updateProfileItems = new List(); @@ -433,7 +561,7 @@ private async Task MigrateProfileExtraSub(List batch) } } - private async Task MigrateProfileExtraGroup() + private async Task MigrateProfileExtraGroupV2ToV3() { var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); var groupItems = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!)); @@ -500,6 +628,7 @@ private async Task MigrateProfileExtraGroup() //await ProfileGroupItemManager.Instance.ClearAll(); } + #pragma warning restore CS0618 #endregion SqliteHelper diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs index b1c8d82a444..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) + public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, + List? verifyPeerCertByName = null, bool allowInsecure = false) { try { @@ -215,12 +203,15 @@ public class CertPemManager using var client = new TcpClient(); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); - await using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); + var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => + ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], + allowInsecure)); + await using var ssl = new SslStream(client.GetStream(), false, callback); var sslOptions = new SslClientAuthenticationOptions { TargetHost = serverName, - RemoteCertificateValidationCallback = ValidateServerCertificate + RemoteCertificateValidationCallback = callback, }; await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); @@ -247,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) + public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, + List? verifyPeerCertByName = null, bool allowInsecure = false) { var pemList = new List(); try @@ -262,12 +254,15 @@ public class CertPemManager using var client = new TcpClient(); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); - await using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); + var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => + ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? [], + allowInsecure)); + await using var ssl = new SslStream(client.GetStream(), false, callback); var sslOptions = new SslClientAuthenticationOptions { TargetHost = serverName, - RemoteCertificateValidationCallback = ValidateServerCertificate + RemoteCertificateValidationCallback = callback, }; await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); @@ -297,35 +292,42 @@ public class CertPemManager } /// - /// Validate server certificate with CA pinning + /// Validate server certificate with CA pinning /// - private bool ValidateServerCertificate( - object sender, + private static bool ValidateServerCertificate( + object _, X509Certificate? certificate, X509Chain? chain, - SslPolicyErrors sslPolicyErrors) + SslPolicyErrors sslPolicyErrors, + List verifyPeerCertByName, + bool allowInsecure) { if (certificate == null) { return false; } - // Check certificate name mismatch - if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) + // In insecure mode, accept any certificate so self-signed certs can be fetched. + if (allowInsecure) { - return false; + return true; } // 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) @@ -333,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) @@ -347,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 @@ -400,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) @@ -420,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 2e11ccab7a0..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); @@ -68,6 +112,8 @@ private void InitCoreInfo() DownloadUrlWinArm64 = urlN + "/download/{0}/v2rayN-windows-arm64.zip", 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", }, @@ -111,6 +157,8 @@ private void InitCoreInfo() DownloadUrlWinArm64 = urlXray + "/download/{0}/Xray-windows-arm64-v8a.zip", 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", @@ -133,6 +181,8 @@ private void InitCoreInfo() DownloadUrlWinArm64 = urlMihomo + "/download/{0}/mihomo-windows-arm64-{0}.zip", 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", @@ -175,6 +225,8 @@ private void InitCoreInfo() DownloadUrlWinArm64 = urlSingbox + "/download/{0}/sing-box-{1}-windows-arm64.zip", 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", @@ -265,6 +317,8 @@ private static string GetCoreUrl(ECoreType eCoreType) names.Add("mihomo-linux-amd64-v1"); 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 14a9ae50aac..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; @@ -90,6 +91,9 @@ public async Task LoadCore(CoreConfigContext? mainContext, CoreConfigContext? pr await CoreStart(mainContext); await CoreStartPreService(preContext); + + AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType; + if (_processService != null) { await UpdateFunc(true, $"{node.GetSummary()}"); @@ -172,7 +176,7 @@ public async Task CoreStop() private async Task CoreStart(CoreConfigContext context) { var node = context.Node; - var coreType = AppManager.Instance.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType); + var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog; 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 93% rename from v2rayN/ServiceLib/Models/ConfigItems.cs rename to v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index 0f0e257cc46..7a0f2d87248 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 @@ -7,14 +7,14 @@ public class CoreBasicItem public string Loglevel { get; set; } - public bool MuxEnabled { get; set; } - - public bool DefAllowInsecure { get; set; } - public string DefFingerprint { get; set; } public string DefUserAgent { get; set; } + public string? SendThrough { get; set; } + + public string? BindInterface { get; set; } + public bool EnableFragment { get; set; } public bool EnableCacheFile4Sbox { get; set; } = true; @@ -47,11 +47,9 @@ public class KcpItem public int DownlinkCapacity { get; set; } - public bool Congestion { get; set; } - - public int ReadBufferSize { get; set; } + public int CwndMultiplier { get; set; } - public int WriteBufferSize { get; set; } + public int MaxSendingWindow { get; set; } } [Serializable] @@ -146,6 +144,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] @@ -156,6 +155,9 @@ public class SpeedTestItem public string SpeedPingTestUrl { get; set; } 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] @@ -195,7 +197,7 @@ public class HysteriaItem { public int UpMbps { get; set; } public int DownMbps { get; set; } - public int HopInterval { get; set; } = 30; + public int HopInterval { get; set; } = Global.Hysteria2DefaultHopInt; } [Serializable] diff --git a/v2rayN/ServiceLib/Models/CoreConfigContext.cs b/v2rayN/ServiceLib/Models/CoreConfigs/CoreConfigContext.cs similarity index 62% rename from v2rayN/ServiceLib/Models/CoreConfigContext.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/CoreConfigContext.cs index a9a49f961e6..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 { @@ -16,10 +16,8 @@ public record CoreConfigContext // TUN Compatibility public bool IsTunEnabled { get; init; } = false; - public HashSet ProtectDomainList { get; init; } = new(); - // -> tun inbound --(if routing proxy)--> relay outbound - // -> proxy core (relay inbound --> proxy outbound --(dialerProxy)--> protect outbound) - // -> protect inbound -> direct proxy outbound data -> internet - public int TunProtectSsPort { get; init; } = 0; - public int ProxyRelaySsPort { get; init; } = 0; + public HashSet ProtectDomainList { get; init; } = []; + + public bool IsWindows { get; init; } + public bool IsMacOS { get; init; } } diff --git a/v2rayN/ServiceLib/Models/CoreInfo.cs b/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs similarity index 83% rename from v2rayN/ServiceLib/Models/CoreInfo.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs index eb4404cb7e9..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 @@ -12,6 +12,8 @@ public class CoreInfo public string? DownloadUrlWinArm64 { get; set; } 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 96% rename from v2rayN/ServiceLib/Models/SingboxConfig.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs index f2331cc4721..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 @@ -173,7 +174,7 @@ public class Peer4Sbox public string? pre_shared_key { get; set; } public List allowed_ips { get; set; } public int? persistent_keepalive_interval { get; set; } - public List reserved { get; set; } + public List? reserved { get; set; } } public class Tls4Sbox @@ -237,6 +238,9 @@ public class Transport4Sbox public class Headers4Sbox { public string? Host { get; set; } + + [JsonPropertyName("User-Agent")] + public string UserAgent { get; set; } } public class HyObfs4Sbox @@ -259,14 +263,6 @@ public class Server4Sbox : BaseServer4Sbox // public List? path { get; set; } // hosts public Dictionary>? predefined { get; set; } - - // Deprecated in sing-box 1.12.0 , kept for backward compatibility - public string? address { get; set; } - - public string? address_resolver { get; set; } - public string? address_strategy { get; set; } - public string? strategy { get; set; } - // Deprecated End } public class Experimental4Sbox diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs similarity index 89% rename from v2rayN/ServiceLib/Models/V2rayConfig.cs rename to v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index 983cb4dae4c..52cab623f3f 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 { @@ -47,9 +47,9 @@ public class Inbounds4Ray { public string tag { get; set; } - public int port { get; set; } + public int? port { get; set; } - public string listen { get; set; } + public string? listen { get; set; } public string protocol { get; set; } @@ -75,6 +75,18 @@ public class Inboundsettings4Ray public bool? allowTransparent { get; set; } public List? accounts { get; set; } + + public string? name { get; set; } + + public int? MTU { get; set; } + + public List? gateway { get; set; } + + public List? autoSystemRoutingTable { get; set; } + + public string? autoOutboundsInterface { get; set; } + + public List? dns { get; set; } } public class UsersItem4Ray @@ -105,6 +117,8 @@ public class Outbounds4Ray public string protocol { get; set; } + public string? sendThrough { get; set; } + public string? targetStrategy { get; set; } public Outboundsettings4Ray settings { get; set; } @@ -126,11 +140,10 @@ public class Outboundsettings4Ray public int? userLevel { get; set; } - public FragmentItem4Ray? fragment { get; set; } - public string? secretKey { get; set; } - public Object? address { get; set; } + public object? address { get; set; } + public int? port { get; set; } public List? peers { get; set; } @@ -150,6 +163,7 @@ public class WireguardPeer4Ray { public string endpoint { get; set; } public string publicKey { get; set; } + public string? preSharedKey { get; set; } } public class VnextItem4Ray @@ -262,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 @@ -317,7 +332,7 @@ public class StreamSettings4Ray public TlsSettings4Ray? tlsSettings { get; set; } - public TcpSettings4Ray? tcpSettings { get; set; } + public RawSettings4Ray? rawSettings { get; set; } public KcpSettings4Ray? kcpSettings { get; set; } @@ -358,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; } @@ -371,7 +387,7 @@ public class CertificateSettings4Ray public string? usage { get; set; } } -public class TcpSettings4Ray +public class RawSettings4Ray { public Header4Ray header { get; set; } } @@ -395,11 +411,9 @@ public class KcpSettings4Ray public int downlinkCapacity { get; set; } - public bool congestion { get; set; } - - public int readBufferSize { get; set; } + public int cwndMultiplier { get; set; } - public int writeBufferSize { get; set; } + public int maxSendingWindow { get; set; } } public class WsSettings4Ray @@ -487,8 +501,27 @@ public class Mask4Ray public class MaskSettings4Ray { + public string? header { get; set; } + public string? value { get; set; } + public string? password { get; set; } - public string? domain { get; set; } + + // fragment + public string? packets { get; set; } + + public string? length { get; set; } + public string? delay { get; set; } + + // noise + public int? reset { get; set; } + + public List? noise { get; set; } +} + +public class NoiseMask4Ray +{ + public string? rand { get; set; } + public string? delay { get; set; } } public class QuicParams4Ray @@ -509,6 +542,9 @@ public class AccountsItem4Ray public class Sockopt4Ray { public string? dialerProxy { get; set; } + + [JsonPropertyName("interface")] + public string? Interface { get; set; } } public class FragmentItem4Ray 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 84% rename from v2rayN/ServiceLib/Models/ProfileItem.cs rename to v2rayN/ServiceLib/Models/Entities/ProfileItem.cs index 50ac8b90944..5daca9888a0 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProfileItem.cs @@ -1,15 +1,16 @@ -namespace ServiceLib.Models; +namespace ServiceLib.Models.Entities; [Serializable] public class ProfileItem { private ProtocolExtraItem? _protocolExtraCache; + private TransportExtraItem? _transportExtraCache; public ProfileItem() { IndexId = string.Empty; ConfigType = EConfigType.VMess; - ConfigVersion = 3; + ConfigVersion = 4; Subid = string.Empty; Address = string.Empty; Port = 0; @@ -17,9 +18,6 @@ public ProfileItem() Username = string.Empty; Network = string.Empty; Remarks = string.Empty; - HeaderType = string.Empty; - RequestHost = string.Empty; - Path = string.Empty; StreamSecurity = string.Empty; AllowInsecure = string.Empty; } @@ -126,20 +124,31 @@ public bool IsValid() return true; } + public ProtocolExtraItem GetProtocolExtra() + { + return _protocolExtraCache ??= JsonUtils.Deserialize(ProtoExtra) ?? new ProtocolExtraItem(); + } + public void SetProtocolExtra(ProtocolExtraItem extraItem) { _protocolExtraCache = extraItem; ProtoExtra = JsonUtils.Serialize(extraItem, false); } - public void SetProtocolExtra() + public TransportExtraItem GetTransportExtra() { - ProtoExtra = JsonUtils.Serialize(_protocolExtraCache, false); + return _transportExtraCache ??= JsonUtils.Deserialize(TransportExtra) ?? new TransportExtraItem(); } - public ProtocolExtraItem GetProtocolExtra() + public void SetTransportExtra(TransportExtraItem transportExtra) { - return _protocolExtraCache ??= JsonUtils.Deserialize(ProtoExtra) ?? new ProtocolExtraItem(); + _transportExtraCache = transportExtra; + TransportExtra = JsonUtils.Serialize(transportExtra, false); + } + + public bool GetAllowInsecure() + { + return AllowInsecure == Global.StringTrue; } #endregion function @@ -160,9 +169,16 @@ public ProtocolExtraItem GetProtocolExtra() public string Password { get; set; } public string Username { get; set; } public string Network { get; set; } + + [Obsolete("Use TransportExtra.RawHeaderType/XhttpMode/GrpcMode/KcpHeaderType instead.")] public string HeaderType { get; set; } + + [Obsolete("Use TransportExtra.Host/GrpcAuthority instead.")] public string RequestHost { get; set; } + + [Obsolete("Use TransportExtra.Path/GrpcServiceName/KcpSeed instead.")] public string Path { get; set; } + public string StreamSecurity { get; set; } public string AllowInsecure { get; set; } public string Sni { get; set; } @@ -172,15 +188,19 @@ public ProtocolExtraItem GetProtocolExtra() public string ShortId { get; set; } public string SpiderX { get; set; } public string Mldsa65Verify { get; set; } + + [Obsolete("Use TransportExtra.XhttpExtra instead.")] public string Extra { get; set; } + public bool? MuxEnabled { get; set; } public string Cert { get; set; } public string CertSha { get; set; } public string EchConfigList { get; set; } - public string EchForceQuery { get; set; } + public string VerifyPeerCertByName { get; set; } public string Finalmask { get; set; } public string ProtoExtra { get; set; } + public string TransportExtra { get; set; } [Obsolete("Use ProtocolExtraItem.Ports instead.")] public string Ports { 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/Entities/TransportExtraItem.cs b/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs new file mode 100644 index 00000000000..b454f0228c6 --- /dev/null +++ b/v2rayN/ServiceLib/Models/Entities/TransportExtraItem.cs @@ -0,0 +1,19 @@ +namespace ServiceLib.Models.Entities; + +public record TransportExtraItem +{ + public string? RawHeaderType { get; init; } + + public string? Host { get; init; } + public string? Path { get; init; } + public string? XhttpMode { get; init; } + public string? XhttpExtra { get; init; } + + public string? GrpcAuthority { get; init; } + public string? GrpcServiceName { get; init; } + public string? GrpcMode { get; init; } + + public string? KcpHeaderType { get; init; } + public string? KcpSeed { get; init; } + public int? KcpMtu { get; init; } +} diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 4498d9999bf..6880de14f89 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -106,7 +106,7 @@ public static string CertSet { } /// - /// 查找类似 Please check the Configuration settings first. 的本地化字符串。 + /// 查找类似 Invalid configuration, please check or reselect 的本地化字符串。 /// public static string CheckServerSettings { get { @@ -222,6 +222,15 @@ public static string FillCorrectDNSText { } } + /// + /// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。 + /// + public static string FillCorrectSendThroughIPv4 { + get { + return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture); + } + } + /// /// 查找类似 Please enter the correct port format. 的本地化字符串。 /// @@ -555,6 +564,15 @@ public static string LvTestDelay { } } + /// + /// 查找类似 IP Info 的本地化字符串。 + /// + public static string LvTestIpInfo { + get { + return ResourceManager.GetString("LvTestIpInfo", resourceCulture); + } + } + /// /// 查找类似 Speed (MB/s) 的本地化字符串。 /// @@ -861,6 +879,15 @@ public static string menuBackupAndRestore { } } + /// + /// 查找类似 Only Check 的本地化字符串。 + /// + public static string menuCheckOnly { + get { + return ResourceManager.GetString("menuCheckOnly", resourceCulture); + } + } + /// /// 查找类似 Check Update 的本地化字符串。 /// @@ -1014,6 +1041,15 @@ public static string menuExport2ClientConfigClipboard { } } + /// + /// 查找类似 Export v2rayN Internal Share Link to Clipboard 的本地化字符串。 + /// + public static string menuExport2InnerUri { + get { + return ResourceManager.GetString("menuExport2InnerUri", resourceCulture); + } + } + /// /// 查找类似 Export Share Link to Clipboard 的本地化字符串。 /// @@ -1284,6 +1320,15 @@ public static string menuMsgViewSelectAll { } } + /// + /// 查找类似 New Update 的本地化字符串。 + /// + public static string menuNewUpdate { + get { + return ResourceManager.GetString("menuNewUpdate", resourceCulture); + } + } + /// /// 查找类似 Open the storage location 的本地化字符串。 /// @@ -1851,6 +1896,15 @@ public static string menuTestServerResult { } } + /// + /// 查找类似 Test Configurations UDP Delay 的本地化字符串。 + /// + public static string menuUdpTestServer { + get { + return ResourceManager.GetString("menuUdpTestServer", resourceCulture); + } + } + /// /// 查找类似 {0} Website 的本地化字符串。 /// @@ -1860,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}' 的本地化字符串。 /// @@ -2013,6 +2085,15 @@ public static string MsgNeedUrl { } } + /// + /// 查找类似 Not Support 的本地化字符串。 + /// + public static string MsgNotSupport { + get { + return ResourceManager.GetString("MsgNotSupport", resourceCulture); + } + } + /// /// 查找类似 Not support protocol '{0}' 的本地化字符串。 /// @@ -2031,6 +2112,15 @@ public static string MsgNoValidSubscription { } } + /// + /// 查找类似 Conflict between {0} and {1} 的本地化字符串。 + /// + public static string MsgOptionsConflict { + get { + return ResourceManager.GetString("MsgOptionsConflict", resourceCulture); + } + } + /// /// 查找类似 Resolved {0} successfully 的本地化字符串。 /// @@ -2148,6 +2238,15 @@ public static string MsgSubscriptionPrevProfileNotFound { } } + /// + /// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。 + /// + public static string MsgTunRouteExcludeInvalidAddress { + get { + return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture); + } + } + /// /// 查找类似 Unpacking... 的本地化字符串。 /// @@ -2571,6 +2670,24 @@ public static string TbAllowInsecure { } } + /// + /// 查找类似 Allow insecure cert fetch (self-signed) 的本地化字符串。 + /// + public static string TbAllowInsecureCertFetch { + get { + return ResourceManager.GetString("TbAllowInsecureCertFetch", resourceCulture); + } + } + + /// + /// 查找类似 Only for fetching self-signed certificates. This may expose you to MITM risks. 的本地化字符串。 + /// + public static string TbAllowInsecureCertFetchTips { + get { + return ResourceManager.GetString("TbAllowInsecureCertFetchTips", resourceCulture); + } + } + /// /// 查找类似 ALPN 的本地化字符串。 /// @@ -2661,6 +2778,15 @@ public static string TbBrowse { } } + /// + /// 查找类似 Camouflage domain 的本地化字符串。 + /// + public static string TbCamouflageDomain { + get { + return ResourceManager.GetString("TbCamouflageDomain", resourceCulture); + } + } + /// /// 查找类似 Cancel 的本地化字符串。 /// @@ -2836,7 +2962,7 @@ public static string TbDNSHostsConfig { } /// - /// 查找类似 Supports DNS Object; Click to view documentation 的本地化字符串。 + /// 查找类似 Please fill in DNS Object; Click to view documentation 的本地化字符串。 /// public static string TbDnsObjectDoc { get { @@ -2898,15 +3024,6 @@ public static string TbEchConfigList { } } - /// - /// 查找类似 EchForceQuery 的本地化字符串。 - /// - public static string TbEchForceQuery { - get { - return ResourceManager.GetString("TbEchForceQuery", resourceCulture); - } - } - /// /// 查找类似 Edit 的本地化字符串。 /// @@ -3078,6 +3195,15 @@ public static string TbHopInt7 { } } + /// + /// 查找类似 Host 的本地化字符串。 + /// + public static string TbHost { + get { + return ResourceManager.GetString("TbHost", resourceCulture); + } + } + /// /// 查找类似 ICMP routing policy 的本地化字符串。 /// @@ -3177,6 +3303,15 @@ public static string TbMldsa65Verify { } } + /// + /// 查找类似 MTU 的本地化字符串。 + /// + public static string TbMtu { + get { + return ResourceManager.GetString("TbMtu", resourceCulture); + } + } + /// /// 查找类似 Transport protocol(network) 的本地化字符串。 /// @@ -3267,6 +3402,15 @@ public static string TbPorts7Tips { } } + /// + /// 查找类似 PreSharedKey 的本地化字符串。 + /// + public static string TbPreSharedKey { + get { + return ResourceManager.GetString("TbPreSharedKey", resourceCulture); + } + } + /// /// 查找类似 Socks port 的本地化字符串。 /// @@ -3385,16 +3529,7 @@ public static string TbRemoteResolveStrategyTips { } /// - /// 查找类似 Camouflage domain(host) 的本地化字符串。 - /// - public static string TbRequestHost { - get { - return ResourceManager.GetString("TbRequestHost", resourceCulture); - } - } - - /// - /// 查找类似 Reserved (2,3,4) 的本地化字符串。 + /// 查找类似 Reserved 的本地化字符串。 /// public static string TbReserved { get { @@ -3421,7 +3556,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 { @@ -3645,6 +3798,24 @@ public static string TbSettingsAutoUpdateInterval { } } + /// + /// 查找类似 Bind Interface 的本地化字符串。 + /// + public static string TbSettingsBindInterface { + get { + return ResourceManager.GetString("TbSettingsBindInterface", resourceCulture); + } + } + + /// + /// 查找类似 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 { + return ResourceManager.GetString("TbSettingsBindInterfaceTip", resourceCulture); + } + } + /// /// 查找类似 Users in China region can ignore this item 的本地化字符串。 /// @@ -3763,7 +3934,7 @@ public static string TbSettingsDefUserAgent { } /// - /// 查找类似 This parameter is valid only for tcp/http and ws 的本地化字符串。 + /// 查找类似 This parameter is valid only for raw/http, ws, gRPC and xhttp 的本地化字符串。 /// public static string TbSettingsDefUserAgentTips { get { @@ -3844,7 +4015,7 @@ public static string TbSettingsEnableCacheFile4Sbox { } /// - /// 查找类似 Check for pre-release updates 的本地化字符串。 + /// 查找类似 Check for pre-release 的本地化字符串。 /// public static string TbSettingsEnableCheckPreReleaseUpdate { get { @@ -4113,15 +4284,6 @@ public static string TbSettingsPass { } } - /// - /// 查找类似 Custom DNS (multiple, separated by commas (,)) 的本地化字符串。 - /// - public static string TbSettingsRemoteDNS { - get { - return ResourceManager.GetString("TbSettingsRemoteDNS", resourceCulture); - } - } - /// /// 查找类似 Route Only 的本地化字符串。 /// @@ -4149,6 +4311,24 @@ public static string TbSettingsSecondLocalPortEnabled { } } + /// + /// 查找类似 Local outbound address (SendThrough) 的本地化字符串。 + /// + public static string TbSettingsSendThrough { + get { + return ResourceManager.GetString("TbSettingsSendThrough", resourceCulture); + } + } + + /// + /// 查找类似 For multi-interface environments, enter the local machine's IPv4 address 的本地化字符串。 + /// + public static string TbSettingsSendThroughTip { + get { + return ResourceManager.GetString("TbSettingsSendThroughTip", resourceCulture); + } + } + /// /// 查找类似 Set Win10 UWP Loopback 的本地化字符串。 /// @@ -4302,15 +4482,6 @@ public static string TbSettingsTunMode { } } - /// - /// 查找类似 MTU 的本地化字符串。 - /// - public static string TbSettingsTunMtu { - get { - return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture); - } - } - /// /// 查找类似 Stack 的本地化字符串。 /// @@ -4338,6 +4509,15 @@ public static string TbSettingsUdpEnabled { } } + /// + /// 查找类似 UDP Test Url 的本地化字符串。 + /// + public static string TbSettingsUdpTestUrl { + get { + return ResourceManager.GetString("TbSettingsUdpTestUrl", resourceCulture); + } + } + /// /// 查找类似 Auth user 的本地化字符串。 /// @@ -4563,6 +4743,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} 的本地化字符串。 /// @@ -4627,7 +4816,7 @@ public static string TipDisplayLog { } /// - /// 查找类似 *Default value tcp 的本地化字符串。 + /// 查找类似 *Default value raw 的本地化字符串。 /// public static string TipNetwork { get { @@ -4645,7 +4834,16 @@ public static string TipPreSocksPort { } /// - /// 查找类似 XHTTP Extra raw JSON, format: { XHTTP Object } 的本地化字符串。 + /// 查找类似 XHTTP Extra 的本地化字符串。 + /// + public static string TransportExtra { + get { + return ResourceManager.GetString("TransportExtra", resourceCulture); + } + } + + /// + /// 查找类似 Raw JSON, format: { XHTTP Object } 的本地化字符串。 /// public static string TransportExtraTip { get { @@ -4654,47 +4852,47 @@ public static string TransportExtraTip { } /// - /// 查找类似 *tcp camouflage type 的本地化字符串。 + /// 查找类似 raw camouflage type 的本地化字符串。 /// - public static string TransportHeaderTypeTip1 { + public static string TransportHeaderType1 { get { - return ResourceManager.GetString("TransportHeaderTypeTip1", resourceCulture); + return ResourceManager.GetString("TransportHeaderType1", resourceCulture); } } /// - /// 查找类似 *kcp camouflage type 的本地化字符串。 + /// 查找类似 kcp camouflage type 的本地化字符串。 /// - public static string TransportHeaderTypeTip2 { + public static string TransportHeaderType2 { get { - return ResourceManager.GetString("TransportHeaderTypeTip2", resourceCulture); + return ResourceManager.GetString("TransportHeaderType2", resourceCulture); } } /// - /// 查找类似 *QUIC camouflage type 的本地化字符串。 + /// 查找类似 QUIC camouflage type 的本地化字符串。 /// - public static string TransportHeaderTypeTip3 { + public static string TransportHeaderType3 { get { - return ResourceManager.GetString("TransportHeaderTypeTip3", resourceCulture); + return ResourceManager.GetString("TransportHeaderType3", resourceCulture); } } /// - /// 查找类似 *grpc mode 的本地化字符串。 + /// 查找类似 gRPC mode 的本地化字符串。 /// - public static string TransportHeaderTypeTip4 { + public static string TransportHeaderType4 { get { - return ResourceManager.GetString("TransportHeaderTypeTip4", resourceCulture); + return ResourceManager.GetString("TransportHeaderType4", resourceCulture); } } /// - /// 查找类似 *xhttp mode 的本地化字符串。 + /// 查找类似 xhttp mode 的本地化字符串。 /// - public static string TransportHeaderTypeTip5 { + public static string TransportHeaderType5 { get { - return ResourceManager.GetString("TransportHeaderTypeTip5", resourceCulture); + return ResourceManager.GetString("TransportHeaderType5", resourceCulture); } } @@ -4726,7 +4924,7 @@ public static string TransportPathTip3 { } /// - /// 查找类似 *grpc service name 的本地化字符串。 + /// 查找类似 gRPC service name 的本地化字符串。 /// public static string TransportPathTip4 { get { @@ -4780,7 +4978,7 @@ public static string TransportRequestHostTip4 { } /// - /// 查找类似 *grpc Authority 的本地化字符串。 + /// 查找类似 gRPC Authority 的本地化字符串。 /// public static string TransportRequestHostTip5 { get { diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 61ccbf7b81b..246417a6086 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -343,7 +343,7 @@ *QUIC key/Kcp seed - *grpc serviceName + gRPC serviceName *هاست http جدا شده با کاما (،) @@ -357,17 +357,17 @@ *QUIC securty - - *tcp camouflage type + + raw camouflage type - - *kcp camouflage type + + kcp camouflage type - - *QUIC camouflage type + + QUIC camouflage type - - *حالت grpc + + حالت grpc TLS @@ -606,9 +606,6 @@ نام مستعار (ملاحظات) - - Camouflage domain(host) - روش رمزگذاری (امنیتی) @@ -619,7 +616,7 @@ TLS - *مقدار پیش فرض tcp + *مقدار پیش فرض raw نوع هسته @@ -723,9 +720,6 @@ مجوز احراز هویت - - سفارشی DNS (multiple, separated by commas (,)) - تنظیم کردن Win10 UWP Loopback @@ -937,7 +931,7 @@ User-Agent - این پارامتر فقط برای tcp/http و ws معتبر است + This parameter is valid only for raw/http, ws, gRPC and xhttp FontFamily (نیاز به راه اندازی مجدد) @@ -1068,7 +1062,7 @@ پشته شبکه - + MTU @@ -1081,7 +1075,7 @@ کلید خصوصی - Reserved (2,3,4) + Reserved آدرس (IPv4, IPv6) @@ -1102,7 +1096,7 @@ پایان تست... - *grpc Authority + RPC Authority افزودن سرور [HTTP] @@ -1320,11 +1314,11 @@ The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart. - - *حالت xhttp + + حالت xhttp - جیسون خام XHTTP Extra, فرمت: { XHTTPObject } + Raw JSON, format: { XHTTP Object } هنگام بستن پنجره در سینی پنهان شوید @@ -1587,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchConfigList - - EchForceQuery - Full certificate (chain), PEM format @@ -1698,4 +1689,61 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Legacy TUN Protect + + Camouflage domain + + + Host + + + XHTTP Extra + + + Allow insecure cert fetch (self-signed) + + + Only for fetching self-signed certificates. This may expose you to MITM risks. + + + Test Configurations UDP Delay + + + UDP Test Url + + + Local outbound address (SendThrough) + + + For multi-interface environments, enter the local machine's IPv4 address + + + Please fill in the correct IPv4 address for SendThrough. + + + Bind Interface + + + For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode + + + PreSharedKey + + + 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 d466eedd577..b97dfcb5a5e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -343,7 +343,7 @@ *clé de chiffrement QUIC - *nom de service gRPC + nom de service gRPC *hôte http, séparés par des virgules (,) @@ -357,17 +357,17 @@ *méthode de chiffrement QUIC - - *type de camouflage tcp + + type de camouflage raw - - *type de camouflage kcp + + type de camouflage kcp - - *type de camouflage QUIC + + type de camouflage QUIC - - *mode gRPC + + mode gRPC TLS @@ -606,9 +606,6 @@ Alias (remarks) - - Domaine de camouflage (host) - Méthode de chiffrement (security) @@ -619,7 +616,7 @@ Sécurité couche transport (TLS) - *tcp par défaut ; un mauvais choix bloque la connexion + *raw par défaut ; un mauvais choix bloque la connexion Type de Core @@ -723,9 +720,6 @@ Mot de passe d’authentification - - DNS perso (plusieurs configurables, séparés par virgules) - Lever la restriction de proxy en boucle locale pour les applications Win10 UWP @@ -937,7 +931,7 @@ Agent utilisateur (User-Agent) - Valable uniquement pour les protocoles tcp/http et ws + This parameter is valid only for raw/http, ws, gRPC and xhttp Police actuelle (redémarrage requis) @@ -1065,7 +1059,7 @@ Pile de protocoles - + MTU @@ -1078,7 +1072,7 @@ PrivateKey - Reserved (2,3,4) + Reserved Address (IPv4,IPv6) @@ -1099,7 +1093,7 @@ Arrêt du test en cours... - *Autorité gRPC + Autorité gRPC Ajouter [HTTP] @@ -1316,12 +1310,12 @@ Le mot de passe sera vérifié en ligne de commande. En cas d’échec ou de dysfonctionnement, redémarrez l’application. Il n’est pas stocké et doit être saisi à chaque redémarrage. - - - *Mode XHTTP + + + Mode XHTTP - JSON brut XHTTP Extra, format : { XHTTPObject } + Raw JSON, format: { XHTTP Object } Masquer dans la barre d’état à la fermeture de la fenêtre @@ -1584,9 +1578,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchConfigList - - EchForceQuery - Certificat complet (chaîne), format PEM @@ -1621,49 +1612,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask - Routing rule {0} outbound node {1} warning: {2} + Règle de routage {0} nœud sortant {1} avertissement: {2} - Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + Règle {0} nœud VPN sortant {1} erreur : {2}. Repli nœud proxy uniquement. - Group {0} has a cycle dependency on child node {1}. Skipping this node. + Le groupe {0} a une dépendance cyclique avec le nœud enfant {1}. Nœud ignoré. - Group {0} child node {1} warning: {2} + Groupe {0} nœud enfant {1} avertissement : {2} - Group {0} child node {1} error: {2}. Skipping this node. + Groupe {0} nœud enfant {1} erreur : {2}. Nœud ignoré. - Group {0} child group node {1} warning: {2} + Groupe {0} nœud enfant groupe {1} avertissement: {2} - Group {0} child group node {1} error: {2}. Skipping this node. + Groupe {0} nœud groupe enfant {1} erreur: {2}. Nœud ignoré. - Group {0} has no valid child node. + Groupe {0} n’a aucun nœud enfant valide. - Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + Règle de routage {0} tag sortant vide. Replié sur le nœud proxy uniquement. - Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + Règle de routage {0} nœud sortant {1} introuvable. Repli sur le seul nœud proxy. - Subscription previous proxy {0} not found. Skipping. + Nœud proxy précédent de l’abonnement {0} introuvable. Ignoré. - Subscription next proxy {0} not found. Skipping. + Nœud proxy suivant de l’abonnement {0} introuvable. Ignoré. - Generate Policy Group + Générer groupe de stratégie - All configurations + Toutes configurations - Group by Region + Grouper par région Copier @@ -1672,7 +1663,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Tout sélect - Paste + Coller Format @@ -1684,15 +1675,72 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Ajouter [NaïveProxy] - Insecure Concurrency + Concurrence non sûre - Username + Nom d’utilisateur - ICMP routing policy + Politique de routage ICMP - Legacy TUN Protect + Protection TUN héritée + + + Domaine de camouflage + + + Host + + + XHTTP Extra + + + Allow insecure cert fetch (self-signed) + + + Pour obtenir des certificats auto-signés uniquement. Risque MITM. + + + Adresse de sortie locale (SendThrough) + + + Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale + + + Veuillez saisir l’adresse IPv4 correcte de SendThrough. + + + Lier l'interface + + + Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN + + + Tester la latence UDP des profils + + + URL de test UDP + + + PreSharedKey + + + 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 89456f99cc1..f1e159c3b45 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -343,7 +343,7 @@ *QUIC kulcs/KCP seed - *grpc szolgáltatásnév + gRPC szolgáltatásnév *http host vesszővel elválasztva (,) @@ -357,17 +357,17 @@ *QUIC biztonság - - *tcp álcázási típus + + raw álcázási típus - - *kcp álcázási típus + + kcp álcázási típus - - *QUIC álcázási típus + + QUIC álcázási típus - - *grpc mód + + gRPC mód TLS @@ -606,9 +606,6 @@ Alias (megjegyzések) - - Álcázási tartomány(host) - Titkosítási módszer (biztonság) @@ -619,7 +616,7 @@ TLS - *Alapértelmezett érték tcp + *Alapértelmezett érték raw Core Típus @@ -723,9 +720,6 @@ Hitelesítési jelszó - - Egyéni DNS (több, vesszővel (,) elválasztva) - Win10 UWP Loopback beállítása @@ -937,7 +931,7 @@ User-Agent - Ez a paraméter csak tcp/http és ws esetén érvényes + This parameter is valid only for raw/http, ws, gRPC and xhttp Betűtípus (újraindítást igényel) @@ -1068,7 +1062,7 @@ Hálózati verem - + MTU @@ -1081,7 +1075,7 @@ Privát kulcs - Fenntartott (2,3,4) + Fenntartott Cím (IPv4, IPv6) @@ -1102,7 +1096,7 @@ Teszt megszakítása... - *grpc Authority + gRPC Authority HTTP konfiguráció hozzáadása @@ -1320,11 +1314,11 @@ A jelszót a parancssoron keresztül ellenőrizzük. Ha egy érvényesítési hiba miatt az alkalmazás hibásan működik, indítsa újra az alkalmazást. A jelszó nem kerül tárolásra, és minden újraindítás után újra meg kell adni. - - *xhttp mód + + xhttp mód - XHTTP Extra nyers JSON, formátum: { XHTTP Objektum } + Raw JSON, format: { XHTTP Object } Ablak bezárásakor a tálcára rejtés @@ -1587,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchConfigList - - EchForceQuery - Full certificate (chain), PEM format @@ -1698,4 +1689,61 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Legacy TUN Protect + + Álcázási tartomány + + + Host + + + XHTTP Extra + + + Allow insecure cert fetch (self-signed) + + + Only for fetching self-signed certificates. This may expose you to MITM risks. + + + Test Configurations UDP Delay + + + UDP Test Url + + + Local outbound address (SendThrough) + + + For multi-interface environments, enter the local machine's IPv4 address + + + Please fill in the correct IPv4 address for SendThrough. + + + Bind Interface + + + For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode + + + PreSharedKey + + + 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 569877d7902..f049ef9833f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -121,7 +121,7 @@ Export share link to clipboard successfully - Please check the Configuration settings first. + Invalid configuration, please check or reselect Invalid configuration format. @@ -343,7 +343,7 @@ *QUIC key/KCP seed - *grpc service name + gRPC service name *http host separated by commas (,) @@ -357,17 +357,17 @@ *QUIC security - - *tcp camouflage type + + raw camouflage type - - *kcp camouflage type + + kcp camouflage type - - *QUIC camouflage type + + QUIC camouflage type - - *grpc mode + + gRPC mode TLS @@ -606,9 +606,6 @@ Alias (remarks) - - Camouflage domain(host) - Encryption method (security) @@ -619,7 +616,7 @@ TLS - *Default value tcp + *Default value raw Core Type @@ -694,7 +691,7 @@ Automatically adjust column width after subscription update - Check for pre-release updates + Check for pre-release Exception @@ -723,9 +720,6 @@ Auth pass - - Custom DNS (multiple, separated by commas (,)) - Set Win10 UWP Loopback @@ -865,7 +859,7 @@ Rule object Doc - Supports DNS Object; Click to view documentation + Please fill in DNS Object; Click to view documentation For group please leave blank here @@ -937,7 +931,7 @@ User-Agent - This parameter is valid only for tcp/http and ws + This parameter is valid only for raw/http, ws, gRPC and xhttp Font family (requires restart) @@ -1068,7 +1062,7 @@ Stack - + MTU @@ -1081,7 +1075,7 @@ Private Key - Reserved (2,3,4) + Reserved Address (IPv4, IPv6) @@ -1102,7 +1096,7 @@ Test terminating... - *grpc Authority + gRPC Authority Add [HTTP] @@ -1320,11 +1314,11 @@ The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart. - - *xhttp mode + + xhttp mode - XHTTP Extra raw JSON, format: { XHTTP Object } + Raw JSON, format: { XHTTP Object } Hide to tray when closing the window @@ -1342,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 @@ -1587,8 +1581,8 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchConfigList - - EchForceQuery + + Verify Peer Cert By Name Full certificate (chain), PEM format @@ -1698,4 +1692,76 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Legacy TUN Protect + + Camouflage domain + + + Host + + + XHTTP Extra + + + Allow insecure cert fetch (self-signed) + + + Only for fetching self-signed certificates. This may expose you to MITM risks. + + + Test Configurations UDP Delay + + + UDP Test Url + + + Local outbound address (SendThrough) + + + For multi-interface environments, enter the local machine's IPv4 address + + + Please fill in the correct IPv4 address for SendThrough. + + + Bind Interface + + + For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode + + + PreSharedKey + + + 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} + + + Conflict between {0} and {1} + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 401c237edbb..f74e45c28ca 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -121,7 +121,7 @@ Ссылка успешно скопирована в буфер обмена - Сначала проверьте настройки сервера + Недопустимая конфигурация, проверьте или выберите заново Недопустимый формат конфигурации @@ -142,7 +142,7 @@ Не удалось получить конфигурацию по умолчанию - Не удалось импортировать сервер пользовательской конфигурации + Не удалось импортировать пользовательскую конфигурацию Не удалось прочитать файл конфигурации @@ -166,7 +166,7 @@ Некорректная конфигурация. Проверьте - Исходная конфигурация + Инициализация конфигурации {0} {1} является последней версией @@ -211,13 +211,13 @@ Не удалось импортировать подписку - Содержимое подписки успешно импортировано + Содержимое подписки успешно получено Нет установленных подписок - Парсинг {0} прошел успешно + Успешно обработано: {0} Начинаю получать подписки @@ -244,7 +244,7 @@ Успешное обновление ядра! Перезапуск службы... - Не является протоколом VMess или Shadowsocks + Протокол не VMess и не Shadowsocks Файл ядра ({1}) не найден в папке {0}. Скачайте по адресу {2} и поместите его туда @@ -253,10 +253,10 @@ Сканирование завершено, не найден корректный QR код - Операция безуспешна, проверьте и попробуйте ещё раз + Операция не удалась, проверьте и повторите попытку - Введите примечания + Введите псевдоним Выберите метод шифрования @@ -268,7 +268,7 @@ Сначала выберите сервер - Удаление дублей завершено. Старая: {0}, Новая: {1} + Дедупликация конфигураций завершена. Было: {0}, Стало: {1}. Вы уверены, что хотите удалить сервер? @@ -283,16 +283,16 @@ Конфигурация выполнена успешно {0} - Пользовательская конфигурация сервера успешно импортирована + Пользовательская конфигурация успешно импортирована - {0} серверов импортировано из буфера обмена + Из буфера обмена импортировано конфигураций: {0} - Сканирование URL-адреса импорта прошло успешно + Ссылка общего доступа успешно отсканирована и импортирована - Задержка текущего сервера: {0} мс, {1} + Задержка: {0} мс, {1} Операция успешна @@ -307,7 +307,7 @@ {0}: одно из обязательных полей - Примечания + Псевдоним URL (необязательно) @@ -334,40 +334,40 @@ Введите корректный пользовательский DNS - *WebSocket-путь + *Путь ws/httpupgrade/xhttp *HTTP2-путь - *QUIC-ключ / KCP-seed + *Ключ шифрования QUIC - Имя сервиса *gRPC + Имя сервиса gRPC *http-хосты, разделённые запятыми (,) - *WebSocket-хост + *Хост ws/httpupgrade/xhttp *HTTP2-хосты, разделённые запятыми (,) - Безопасность *QUIC + *Метод шифрования QUIC - - Тип *TCP-камуфляжа + + Тип raw-камуфляжа - - Тип *KCP-камуфляжа + + Тип KCP-камуфляжа - - Тип *QUIC-камуфляжа + + Тип QUIC-камуфляжа - - Режим *gRPC + + Режим gRPC TLS @@ -382,7 +382,7 @@ Глобальная горячая клавиша {0} зарегистрирована успешно - Все серверы + Все Выберите файл конфигурации сервера для импорта @@ -397,10 +397,10 @@ Локальный - Фильтр серверов + Фильтр, нажмите Enter для выполнения - Проверить обновления + Обновить Закрыть @@ -409,19 +409,19 @@ Выход - Глобальная настройка горячих клавиш + Настройка глобальных горячих клавиш Помощь - Настройка параметров + Параметры - Содействие + Продвижение - Перезагрузка + Перезапустить службу Настройки маршрутизации @@ -436,19 +436,19 @@ Обновить текущую подписку без прокси - Обновить подписку через прокси + Обновить текущую подписку через прокси Группа подписки - Настройки группы подписки + Настройки групп подписки - Обновить подписку без прокси + Обновить все подписки без прокси - Обновить подписку с прокси + Обновить все подписки через прокси Системный прокси @@ -472,46 +472,46 @@ Язык (требуется перезапуск) - Импорт массива URL из буфера обмена + Импорт ссылок общего доступа из буфера обмена - Сканировать QR-код с экрана + Сканировать QR-код на экране - Клонировать выбранный сервер + Клонировать выбранное - Удалить дубликаты серверов + Удалить дубликаты - Удалить выбранные серверы + Удалить выбранное - Установить как активный сервер + Сделать активным - Очистить всю статистику + Очистить статистику всех сервисов - Тест на реальную задержку сервера + Тест реальной задержки - Сортировать по результату теста + Сортировка по результату теста - Тест на скорость загрузки сервера + Тест скорости загрузки - Тест задержки с tcping + Тест tcping - Экспортировать выбранный сервер для клиента + Экспортировать выбранное как полную конфигурацию - Экспорт URL-адресов общего доступа в буфер обмена + Копировать ссылку общего доступа в буфер обмена - Добавить сервер пользовательской конфигурации + Добавить пользовательскую конфигурацию Добавить сервер [Shadowsocks] @@ -556,13 +556,13 @@ Поделиться - Включены обновления + Включить обновление Сортировка - Заголовок User-Agent + User-Agent (необязательно) Отмена @@ -577,7 +577,7 @@ Адрес - Разрешить небезопасные + Пропустить проверку сертификата (allowInsecure) ALPN @@ -595,7 +595,7 @@ UUID (id) - Транспортный протокол сети + Транспортный протокол (network) Путь @@ -604,13 +604,10 @@ Порт - Примечание - - - Маскирующий домен (хост) + Псевдоним (remarks) - Метод шифрования + Метод шифрования (security) SNI @@ -619,13 +616,13 @@ TLS - *По-умолчанию TCP + *По умолчанию raw; при неверном выборе подключение будет невозможно - Ядро + Тип ядра - Поток + Управление потоком (Flow) Генерировать @@ -634,7 +631,7 @@ Пароль - Пароль(Необязательно) + Пароль (необязательно) UUID(id) @@ -643,19 +640,19 @@ Шифрование - Пользователь(Необязательно) + Имя пользователя (необязательно) - Шифрования + Шифрование Порт SOCKS - * После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости + *Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций - Просмотр + Обзор Редактировать @@ -667,7 +664,7 @@ Разрешить подключения из локальной сети - Автоскрытие при автозапуске + Автоматически скрывать при запуске Интервал автоматического обновления Geo в часах @@ -676,7 +673,7 @@ Ядро: базовые настройки - V2ray Custom DNS + Пользовательский DNS для v2ray Ядро: настройки KCP @@ -685,7 +682,7 @@ Настройки типа ядра - Разрешить небезопасные + По умолчанию пропускать проверку сертификата (allowInsecure) «Freedom»: стратегия обработки доменов исходящего трафика @@ -700,13 +697,13 @@ Исключение - Исключение. Не используйте прокси-сервер для адресов, начинающихся с (,), используйте точку с запятой (;) + Исключения: не использовать прокси для адресов, начинающихся с указанных. Разделяйте точкой с запятой (;) - Показывать скорость в реальном времени + Показывать скорость в реальном времени (требуется перезапуск) - Сохранить старые при удалении дублей + При удалении дубликатов сохранять элементы с меньшим порядковым номером Записывать логи @@ -721,10 +718,7 @@ Настройки v2rayN - Пароль аутентификации - - - Пользовательский DNS (если несколько, то делите запятыми (,)) + Пароль авторизации Разрешить loopback для приложений UWP (Win10) @@ -736,43 +730,43 @@ Смешанный порт - Автозапуск + Запускать при старте системы (может не сработать) - Включить статистику (требуется перезагрузка) + Включить статистику трафика (требуется перезапуск) - URL конвертации подписок + URL конвертации подписок (необязательно) Настройки системного прокси - Лимит серверов в меню трея + Лимит отображения серверов в контекстном меню трея Включить UDP - Имя пользователя (логин) + Пользователь авторизации Очистить системный прокси - Показать GUI + Показать интерфейс - Настройка горячих клавиш + Настройка глобальных горячих клавиш - Установите непосредственно, нажав на клавиатуру, вступит в силу после перезапуска + Задайте, нажав нужную комбинацию клавиш; вступит в силу после перезапуска - Не изменять системный прокси + Не менять системный прокси - Обнулить + Сбросить Установить системный прокси @@ -781,25 +775,25 @@ Режим PAC - Поделиться сервером + Поделиться Маршрутизация - Пользователь + Запущено без прав администратора - Администратор + Запущено от имени администратора - Спуститься вниз + Переместить в самый низ Вниз - Подняться наверх + Переместить в самый верх Вверх @@ -808,13 +802,13 @@ Фильтр, поддерживает regex - {0} веб-сайт + Веб-сайт {0} - Добавить + Добавить набор правил - Добавить расширенные правила + Импортировать правила Удалить выбранные @@ -847,7 +841,7 @@ Добавить правило - Экспорт выделенных правил + Экспортировать выбранные правила в буфер обмена Список правил @@ -859,16 +853,16 @@ Детальные настройки правил маршрутизации - Домен и IP автоматически сортируются при сохранении + Домен, IP и процесс автоматически сортируются при сохранении Документация RuleObject - Поддерживаются DNS-объекты, нажмите для просмотра документации + Заполните DNS-объект (формат JSON), нажмите для просмотра документации - Необязательное поле + Для группы оставьте это поле пустым Настройки маршрутизации изменены @@ -877,13 +871,13 @@ Системные прокси изменены - Только маршрут + Только для маршрутизации (routeOnly) - Не используйте прокси-серверы для локальных (интранет) адресов + Не использовать прокси для локальных (интранет) адресов - Тест задержки и скорости всех серверов (Ctrl+E) + Многопоточный тест задержки и скорости (Ctrl+E) Задержка (мс) @@ -892,16 +886,16 @@ Скорость (МБ/с) - Не удалось запустить ядро, посмотрите логи + Не удалось запустить ядро, см. сообщение - Фильтр примечаний (Regex) + Фильтр по псевдониму (регулярные выражения) - Показать логи + Отображать журнал - Режим VPN + Включить TUN Новый порт для локальной сети @@ -910,10 +904,10 @@ Настройки режима TUN - Перейти в группу + Переместить в группу - Включить сортировку перетаскиванием сервера (требуется перезагрузка) + Включить сортировку перетаскиванием сервера (требуется перезапуск) Автообновление @@ -922,10 +916,10 @@ Пропустить тест - Редактировать сервер + Редактировать - Двойной клик чтобы сделать сервер активным + Двойной клик для активации конфигурации Тест завершен @@ -937,19 +931,19 @@ User-Agent - Параметр действует только для TCP/HTTP и WebSocket (WS) + Параметр действует только для raw/http, ws, gRPC и xhttp - Шрифт (требуется перезагрузка) + Шрифт (требуется перезапуск) - Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек + Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение - Pac порт = +3,Xray API порт = +4, mihomo API порт = +5 + PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5; - Установите это с правами администратора + Установите с правами администратора; после запуска приложение получит права администратора Размер шрифта @@ -964,7 +958,7 @@ Переместить вверх/вниз - PublicKey + Открытый ключ ShortId @@ -973,22 +967,22 @@ SpiderX - Включить аппаратное ускорение (требуется перезагрузка) + Включить аппаратное ускорение (требуется перезапуск) - Ожидание тестирования… + Ожидание… - нажмите ESC для отмены + Нажмите ESC для прекращения теста - Отключите при аномальном разрыве соединения + Отключите при нестабильном соединении Обновления не включены — подписка пропущена - Перезагрузить как администратор + Перезапустить от имени администратора Дополнительные URL через запятую, конвертация подписки недоступна @@ -1003,7 +997,7 @@ Включить запись логов в файл - Преобразовать тип цели + Целевой тип конвертации Если преобразование не требуется, оставьте поле пустым @@ -1012,10 +1006,10 @@ Настройки DNS - sing-box Custom DNS + Пользовательский DNS для sing-box - Заполните структуру DNS, нажмите, чтобы открыть документ + Заполните структуру DNS (формат JSON), нажмите для просмотра документации Нажмите, чтобы импортировать конфигурацию DNS по умолчанию @@ -1039,25 +1033,25 @@ Добавить сервер [Hysteria2] - Максимальная пропускная способность Hysteria (загрузка/отдача) + Максимальная пропускная способность Hysteria (отдача/загрузка) - Использовать системные узлы + Использовать системный файл hosts Добавить сервер [TUIC] - Контроль перегрузок + Управление перегрузками - Примечания к предыдущему прокси + Псевдоним предыдущего прокси - Примечания к следующему прокси + Псевдоним следующего прокси - Убедитесь, что примечание существует и является уникальным + Убедитесь, что псевдоним существует и является уникальным Автоматическая маршрутизация @@ -1068,7 +1062,7 @@ Сетевой стек - + MTU @@ -1081,10 +1075,10 @@ Приватный ключ - Зарезервировано (2, 3, 4) + Зарезервировано - Адрес (Ipv4,Ipv6) + Адрес (IPv4, IPv6) Пароль obfs @@ -1099,10 +1093,10 @@ URL для быстрой проверки реальной задержки - Отмена тестирования... + Завершение тестирования... - * gRPC Authority (HTTP/2 псевдозаголовок :authority) + gRPC Authority (HTTP/2 псевдозаголовок :authority) Добавить сервер [HTTP] @@ -1117,7 +1111,7 @@ Пользовательский набор правил для sing-box - Операция успешна. Перезапустите приложение + Операция успешна. Закройте приложение, нажав «Выход» в меню трея, и запустите его заново Открыть место хранения @@ -1138,10 +1132,10 @@ Скорость загрузки - Скачанный трафик + Загруженный трафик - Узел + Хост Имя @@ -1183,7 +1177,7 @@ Глобальный режим - Не менять + Как в исходной конфигурации Правила @@ -1204,7 +1198,7 @@ Стратегия домена по умолчанию для исходящих - Основная ориентация макета (требуется перезагрузка) + Основная ориентация макета (требуется перезапуск) Исходящий DNS адрес @@ -1216,13 +1210,13 @@ Экспорт ссылок в формате Base64 в буфер обмена - Экспортировать выбранный сервер для полной конфигурации в буфер обмена + Экспортировать выбранную полную конфигурацию в буфер обмена Показать или скрыть главное окно - Пользовательская конфигурация порта SOCKS + Порт SOCKS для пользовательской конфигурации Резервное копирование и восстановление @@ -1276,13 +1270,13 @@ Источник файлов наборов правил sing-box (необязательно) - Программы для обновления не существует + Приложение для обновления не найдено - Источник правил маршрутизации + Источник правил маршрутизации (необязательно) - Региональные пресеты + Настройка региональных пресетов По умолчанию (Китай) @@ -1300,13 +1294,13 @@ Сканировать QR-код с изображения - Неверный адрес (Url) + Неверный адрес (URL) Не используйте небезопасный адрес подписки по протоколу HTTP - Установите шрифт в систему и перезапустите настройки + Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение Вы уверены, что хотите выйти? @@ -1320,20 +1314,20 @@ Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска. - - *XHTTP-режим + + XHTTP-режим - Дополнительный „сырой“ JSON для XHTTP, формат: { XHTTP Object } + Сырой JSON, формат: { XHTTPObject } - Скрыть в трее при закрытии окна + Сворачивать в трей при закрытии окна - Количество одновременно выполняемых тестов при многоэтапном тестировании + Количество параллельных задач при многопоточном тестировании - Исключение. Не используйте прокси-сервер для адресов с запятой (,) + Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,) Тип сниффинга @@ -1342,10 +1336,10 @@ Включить второй смешанный порт - socks: локальный порт, socks2: второй локальный порт, socks3: LAN порт + tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт - Темы + Тема Копировать команду прокси в буфер обмена @@ -1363,7 +1357,7 @@ Удалено {0} недействительных - Диапазон портов сервера + Диапазон портов для переключения (Port Hopping) Заменит указанный порт, перечисляйте через запятую (,) @@ -1375,7 +1369,7 @@ URL для тестирования текущего соединения - Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально + Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален Неверный пароль, попробуйте ещё раз. @@ -1384,13 +1378,13 @@ Mldsa65Verify - Добавить сервер [Anytls] + Добавить сервер [AnyTLS] Удалённый DNS - Внутренний DNS + DNS для прямых подключений Стратегия разрешения прямых соединений @@ -1495,13 +1489,13 @@ Тип группы политик - Добавить группу политик + Добавить группу политик Добавить цепочку прокси - Добавить дочернюю конфигурацию + Добавить дочернюю конфигурацию Удалить дочернюю конфигурацию @@ -1540,7 +1534,7 @@ Bootstrap DNS - Разрешает домены DNS-серверов, требуется IP-адрес + Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес Тест реальной задержки @@ -1553,7 +1547,7 @@ Привязанный сертификат (заполните любое из полей) -При указании сертификат будет привязан, а «Разрешить небезопасные» отключится. +При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится. Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе. @@ -1587,9 +1581,6 @@ EchConfigList - - EchForceQuery - Полный сертификат (цепочка) в формате PEM @@ -1675,27 +1666,84 @@ Выбрать все - Paste + Вставить - Format + Форматировать - UDP over TCP + UDP поверх TCP Добавить сервер [NaïveProxy] - Insecure Concurrency + Небезопасная конкурентность (Insecure Concurrency) - Username + Имя пользователя - ICMP routing policy + Политика маршрутизации ICMP - Legacy TUN Protect + Устаревшая защита TUN (Legacy Protect) + + + Камуфляжный домен + + + Хост + + + Дополнительные параметры XHTTP (Extra) + + + Разрешить небезопасную загрузку сертификата (самоподписанного) + + + Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM). + + + Тест UDP-задержки конфигураций + + + URL для UDP-теста + + + Локальный исходящий адрес (SendThrough) + + + Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера + + + Укажите корректный IPv4-адрес для SendThrough. + + + Привязать интерфейс + + + Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN + + + Общий ключ (PSK) + + + Экспорт внутренней ссылки 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 8c628644580..28337430c55 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -121,7 +121,7 @@ 导出分享链接至剪贴板成功 - 请先检查设置 + 配置项无效,请检查或重新选择 配置格式不正确 @@ -343,7 +343,7 @@ *QUIC 加密密钥 - *grpc serviceName + gRPC serviceName *http host 中间逗号 (,) 分隔 @@ -357,17 +357,17 @@ *QUIC 加密方式 - - *tcp 伪装类型 + + raw 伪装类型 - - *kcp 伪装类型 + + kcp 伪装类型 - - *QUIC 伪装类型 + + QUIC 伪装类型 - - *grpc 模式 + + gRPC 模式 TLS @@ -606,9 +606,6 @@ 别名 (remarks) - - 伪装域名 (host) - 加密方式 (security) @@ -619,7 +616,7 @@ 传输层安全 (TLS) - *默认 tcp,选错会无法连接 + *默认 raw,选错会无法连接 Core 类型 @@ -694,7 +691,7 @@ 自动调整配置列宽在更新订阅后 - 检查 Pre-Release 更新 (请谨慎启用) + 检查 Pre-Release 例外 @@ -723,9 +720,6 @@ 认证密码 - - 自定义 DNS (可多个,用逗号 (,) 分隔) - 解除 Win10 UWP 应用回环代理限制 @@ -865,7 +859,7 @@ 规则详细说明文档 - 支持填写 DnsObject,JSON 格式,点击查看文档 + 请填写 DnsObject,JSON 格式,点击查看文档 普通分组此处请留空 @@ -937,7 +931,7 @@ 用户代理 (User-Agent) - 仅对 tcp/http、ws 协议生效 + 仅对 raw/http、ws、gRPC、xhttp 生效 当前字体 (需重启) @@ -1065,7 +1059,7 @@ 协议栈 - + MTU @@ -1078,7 +1072,7 @@ PrivateKey - Reserved (2,3,4) + Reserved Address (IPv4,IPv6) @@ -1099,7 +1093,7 @@ 测试终止中... - *grpc Authority + gRPC Authority 添加 [HTTP] @@ -1317,11 +1311,11 @@ 密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。 - - *XHTTP 模式 + + XHTTP 模式 - XHTTP Extra 原始 JSON,格式: { XHTTPObject } + 原始 JSON,格式: { XHTTPObject } 关闭窗口时隐藏至托盘 @@ -1339,7 +1333,7 @@ 开启第二个本地监听端口 - Socks:本地端口,Socks2:第二个本地端口,Socks3:局域网端口 + Tun:TUN 入站,Socks:本地端口,Socks2:第二个本地端口,Socks3:局域网端口 主题 @@ -1584,9 +1578,6 @@ EchConfigList - - EchForceQuery - 完整证书(链),PEM 格式 @@ -1695,4 +1686,79 @@ 旧版 TUN 保护 + + 伪装域名 + + + Host + + + XHTTP Extra + + + 允许不安全获取证书(自签名) + + + 仅用于抓取自签证书,存在中间人风险。 + + + 测试 UDP 延迟 (多选) + + + UDP 测试地址 + + + 本地出站地址 (SendThrough) + + + 用于多网口环境,请填写本机 IPv4 地址 + + + 请填写正确的 SendThrough IPv4 地址。 + + + 绑定网口 + + + 用于多网口环境,填写要绑定的网口名称,仅生效于 Windows 系统或 TUN 模式 + + + PreSharedKey + + + 导出 v2rayN 内部分享链接至剪贴板 (多选) + + + {0} 有新版本可用:{1} + + + 仅检查 + + + 不支持 + + + IP 信息 + + + 有更新 + + + 警告:Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 + + + Verify Peer Cert By Name + + + 路由排除地址 + + + 使用逗号 (,) 分隔 + + + TUN 路由排除列表包含无效地址:{0} + + + {0} 与 {1} 冲突 + \ 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 59a9fc9f60a..2f1c1857cf6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -121,7 +121,7 @@ 匯出分享連結至剪貼簿成功 - 請先檢查設定 + 配置項目無效,請檢查或重新選擇 設定格式不正確 @@ -343,7 +343,7 @@ *QUIC 加密金鑰 - *grpc serviceName + gRPC serviceName *http host 中間逗號 (,) 分隔 @@ -357,17 +357,17 @@ *QUIC 加密方式 - - *TCP 偽裝類型 + + raw 偽裝類型 - - *KCP 偽裝類型 + + KCP 偽裝類型 - - *QUIC 偽裝類型 + + QUIC 偽裝類型 - - *GRPC 模式 + + gRPC 模式 TLS @@ -606,9 +606,6 @@ 別名 (remarks) - - 偽裝域名 (host) - 加密方式 (security) @@ -619,7 +616,7 @@ 傳輸層安全性 (TLS) - *預設 TCP,選錯會無法連線 + *預設 raw,選錯會無法連線 Core 類型 @@ -694,7 +691,7 @@ 在更新訂閱後自動調整列寬 - 檢查 Pre-Release 更新 (請謹慎啟用) + 檢查 Pre-Release 例外 @@ -723,9 +720,6 @@ 認證密碼 - - 自訂 DNS (可多個,用逗號 (,) 分隔) - 解除 Win10 UWP 應用回環代理限制 @@ -865,7 +859,7 @@ 規則詳細說明檔案 - 支援填寫 DnsObject,JSON 格式,點擊查看說明 + 請填寫 DnsObject,JSON 格式,點擊查看說明 普通分組此處請留空 @@ -937,7 +931,7 @@ 使用者代理 (User-Agent) - 僅對 TCP/HTTP、WS 協定生效 + 僅對 raw/HTTP、WS、gRPC、XHTTP 生效 目前字型 (需重啟) @@ -1065,7 +1059,7 @@ 協定堆疊 - + MTU @@ -1078,7 +1072,7 @@ PrivateKey - Reserved (2,3,4) + Reserved Address (Ipv4,Ipv6) @@ -1099,7 +1093,7 @@ 測試終止中... - *grpc Authority + gRPC Authority 新增 [HTTP] 節點 @@ -1317,11 +1311,11 @@ 密碼將調用命令行校驗,如果因為校驗錯誤導致無法正常運行時,請重啟本應用。密碼不會存儲,每次重啟後都需要再次輸入。 - - *xhttp 模式 + + xhttp 模式 - XHTTP Extra 原始 JSON,格式: { XHTTPObject } + 原始 JSON,格式: { XHTTPObject } 關閉視窗時隱藏至托盤 @@ -1584,9 +1578,6 @@ EchConfigList - - EchForceQuery - 完整憑證(鏈),PEM 格式 @@ -1693,6 +1684,75 @@ ICMP 路由策略 - Legacy TUN Protect + 舊版 TUN 保護 + + + 偽裝域名 + + + Host + + + XHTTP Extra + + + 允許不安全獲取證書(自簽名) + + + 僅用於抓取自簽證書,存在中間人風險。 + + + 測試 UDP 延遲(多選) + + + UDP 測試網址 + + + 本機出站位址 (SendThrough) + + + 適用於多網路介面環境,請填寫本機 IPv4 位址 + + + 請填寫正確的 SendThrough IPv4 位址。 + + + 綁定網路介面 + + + 適用於多網路介面環境,請填寫要綁定的介面名稱;Windows 系統有效,其他系統僅在 TUN 模式下生效 + + + PreSharedKey + + + 匯出 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 new file mode 100644 index 00000000000..2edc5ea0643 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SampleTunInbound @@ -0,0 +1,27 @@ +{ + "tag": "tun", + "protocol": "tun", + "settings": { + "name": "xray_tun", + "MTU": 9000, + "gateway": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "dns": [ + "172.18.0.1" + ], + "autoSystemRoutingTable": [ + "0.0.0.0/0", + "::/0" + ], + "autoOutboundsInterface": "auto" + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls" + ] + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SampleTunRules b/v2rayN/ServiceLib/Sample/SampleTunRules new file mode 100644 index 00000000000..ca6eb051251 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SampleTunRules @@ -0,0 +1,14 @@ +[ + { + "network": "udp", + "port": "135,137-139,5353", + "outboundTag": "block" + }, + { + "ip": [ + "224.0.0.0/3", + "ff00::/8" + ], + "outboundTag": "block" + } +] \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/proxy_set_linux_sh b/v2rayN/ServiceLib/Sample/proxy_set_linux_sh index 112bb39fe4a..ac193d09de1 100644 --- a/v2rayN/ServiceLib/Sample/proxy_set_linux_sh +++ b/v2rayN/ServiceLib/Sample/proxy_set_linux_sh @@ -1,5 +1,24 @@ #!/bin/bash +trim() { + local -n ref=$1 + ref="${ref#"${ref%%[![:space:]]*}"}" + ref="${ref%"${ref##*[![:space:]]}"}" +} + +build_gsettings_array() { + [[ -z "$1" ]] && echo "[]" && return + local host joined hosts=() + IFS=',' read -ra parts <<< "$1" + for host in "${parts[@]}"; do + trim host + [[ -n "$host" ]] && hosts+=("$host") + done + [[ ${#hosts[@]} -eq 0 ]] && echo "[]" && return + printf -v joined "'%s'," "${hosts[@]}" + echo "[${joined%,}]" +} + # Function to set proxy for GNOME set_gnome_proxy() { local MODE=$1 @@ -21,7 +40,7 @@ set_gnome_proxy() { done # Set ignored hosts - gsettings set org.gnome.system.proxy ignore-hosts "['$IGNORE_HOSTS']" + gsettings set org.gnome.system.proxy ignore-hosts "$(build_gsettings_array "$IGNORE_HOSTS")" echo "GNOME: Manual proxy settings applied." echo "Proxy IP: $PROXY_IP" 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 8cfbf8ca4a3..5b0b807261b 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -6,11 +6,13 @@ + true - + + @@ -38,6 +40,8 @@ + + @@ -82,4 +86,8 @@ + + + + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs index 9844c4f947e..9cddbfe837d 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs @@ -57,56 +57,13 @@ public RetResult GenerateClientConfigContent() ConvertGeo2Ruleset(); + ApplyOutboundBindInterface(); + ApplyOutboundSendThrough(); + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Success = true; ret.Data = ApplyFullConfigTemplate(); - if (!context.AppConfig.TunModeItem.EnableLegacyProtect - && context.TunProtectSsPort is > 0 and <= 65535) - { - var ssInbound = new - { - type = "shadowsocks", - tag = "tun-protect-ss", - listen = Global.Loopback, - listen_port = context.TunProtectSsPort, - method = "none", - password = "none", - }; - var directRule = new Rule4Sbox() - { - inbound = new List { ssInbound.tag }, - outbound = Global.DirectTag, - }; - var singboxConfigNode = JsonUtils.ParseJson(ret.Data.ToString())!.AsObject(); - var inboundsNode = singboxConfigNode["inbounds"]!.AsArray(); - inboundsNode.Add(JsonUtils.SerializeToNode(ssInbound, new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - })); - var routeNode = singboxConfigNode["route"]?.AsObject(); - var rulesNode = routeNode?["rules"]?.AsArray(); - var protectRuleNode = JsonUtils.SerializeToNode(directRule, - new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); - if (rulesNode != null) - { - rulesNode.Insert(0, protectRuleNode); - } - else - { - var newRulesNode = new JsonArray() { protectRuleNode }; - if (routeNode is null) - { - var newRouteNode = new JsonObject() { ["rules"] = newRulesNode }; - singboxConfigNode["route"] = newRouteNode; - } - else - { - routeNode["rules"] = newRulesNode; - } - } - ret.Data = JsonUtils.Serialize(singboxConfigNode); - } return ret; } catch (Exception ex) @@ -214,6 +171,8 @@ public RetResult GenerateClientSpeedtestConfig(List selecteds) _coreConfig.route.rules.Add(rule); } + ApplyOutboundBindInterface(); + ApplyOutboundSendThrough(); ret.Success = true; ret.Data = JsonUtils.Serialize(_coreConfig); return ret; @@ -272,6 +231,8 @@ public RetResult GenerateClientSpeedtestConfig(int port) listen_port = port, type = EInboundProtocol.mixed.ToString(), }); + ApplyOutboundBindInterface(); + ApplyOutboundSendThrough(); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Success = true; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs index 25b2970191d..4fffe27c7aa 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs @@ -58,4 +58,56 @@ private string ApplyFullConfigTemplate() return JsonUtils.Serialize(fullConfigTemplateNode); } + + private void ApplyOutboundBindInterface() + { + var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); + if (bindInterface.IsNullOrEmpty()) + { + return; + } + if (!(context.IsTunEnabled || context.IsWindows)) + { + return; + } + foreach (var outbound in _coreConfig.outbounds ?? []) + { + outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null; + } + } + + private void ApplyOutboundSendThrough() + { + var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx(); + if (sendThrough.IsNullOrEmpty()) + { + return; + } + foreach (var outbound in _coreConfig.outbounds ?? []) + { + outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null; + } + } + + private static bool ShouldBindNet(Outbound4Sbox outbound) + { + if (outbound.type is "direct" or "block" or "dns" or "selector" or "urltest") + { + return false; + } + + if (!outbound.detour.IsNullOrEmpty()) + { + return false; + } + + var outboundAddress = outbound.server ?? string.Empty; + + if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return !IPAddress.TryParse(outboundAddress, out var address) || !IPAddress.IsLoopback(address); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index f09d3ee94a9..f5a7da4b305 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -298,7 +298,7 @@ private void GenDnsRules() var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; var expectedIPCidr = new List(); var expectedIPsRegions = new List(); - var regionNames = new HashSet(); + var regionName = string.Empty; if (!string.IsNullOrEmpty(simpleDnsItem?.DirectExpectedIPs)) { @@ -310,16 +310,16 @@ private void GenDnsRules() foreach (var ip in ipItems) { - if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase)) + if (ip.StartsWith(Global.GeoIPPrefix, StringComparison.OrdinalIgnoreCase)) { - var region = ip["geoip:".Length..]; - if (!string.IsNullOrEmpty(region)) + var region = ip[Global.GeoIPPrefix.Length..]; + if (string.IsNullOrEmpty(region)) { - expectedIPsRegions.Add(region); - regionNames.Add(region); - regionNames.Add($"geolocation-{region}"); - regionNames.Add($"tld-{region}"); + continue; } + + expectedIPsRegions.Add(region); + regionName = region; } else { @@ -352,19 +352,25 @@ private void GenDnsRules() rule.server = Global.SingboxDirectDNSTag; rule.strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom); - if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0) + if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0 && !regionName.IsNullOrEmpty()) { - var geositeSet = new HashSet(rule.geosite); - if (regionNames.Intersect(geositeSet).Any()) + var regionGeosite = rule.geosite.Where(g => g.EndsWith($"-{regionName}", StringComparison.OrdinalIgnoreCase) + || g.EndsWith($"@{regionName}", StringComparison.OrdinalIgnoreCase) + || g == regionName).ToList(); + if (regionGeosite.Count > 0) { + rule.geosite.RemoveAll(regionGeosite.Contains); + var rule4ExpectedIPs = JsonUtils.DeepCopy(rule); + rule4ExpectedIPs.geosite = regionGeosite; if (expectedIPsRegions.Count > 0) { - rule.geoip = expectedIPsRegions; + rule4ExpectedIPs.geoip = expectedIPsRegions; } if (expectedIPCidr.Count > 0) { - rule.ip_cidr = expectedIPCidr; + rule4ExpectedIPs.ip_cidr = expectedIPCidr; } + _coreConfig.dns.rules.Add(rule4ExpectedIPs); } } } @@ -429,15 +435,7 @@ private void GenDnsCustom() return; } _coreConfig.dns = dns4Sbox; - if (dns4Sbox.servers?.Count > 0 && - dns4Sbox.servers.First().address.IsNullOrEmpty()) - { - GenDnsProtectCustom(); - } - else - { - GenDnsProtectCustomLegacy(); - } + GenDnsProtectCustom(); } catch (Exception ex) { @@ -479,23 +477,6 @@ private void GenDnsProtectCustom() _coreConfig.dns = dns4Sbox; } - private void GenDnsProtectCustomLegacy() - { - GenDnsProtectCustom(); - - _coreConfig.dns?.servers?.RemoveAll(s => s.tag == Global.SingboxLocalDNSTag); - var dnsItem = context.RawDnsItem; - var localDnsServer = new Server4Sbox() - { - address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) - ? Global.DomainPureIPDNSAddress.FirstOrDefault() - : dnsItem?.DomainDNSAddress, - tag = Global.SingboxLocalDNSTag, - detour = Global.DirectTag, - }; - _coreConfig.dns?.servers?.Add(localDnsServer); - } - private Rule4Sbox? BuildProtectDomainRule() { if (context.ProtectDomainList.Count == 0) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs index 3a27c7daefe..3aa06d4e058 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs @@ -8,10 +8,10 @@ private void GenInbounds() { var listen = "0.0.0.0"; var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); + var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort; _coreConfig.inbounds = []; - if (!context.IsTunEnabled - || (context.IsTunEnabled && _node.Port != listenPort)) + if (!context.IsTunEnabled || !isUsingLocalMixedPort) { var inbound = new Inbound4Sbox() { @@ -62,7 +62,7 @@ private void GenInbounds() } var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { }; - tunInbound.interface_name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "singbox_tun"; + tunInbound.interface_name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "singbox_tun"; tunInbound.mtu = _config.TunModeItem.Mtu; tunInbound.auto_route = _config.TunModeItem.AutoRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute; @@ -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/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 0fc9733cce9..31c02a811c1 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -22,7 +22,7 @@ private List BuildAllProxyOutbounds(string baseTagName = Global } if (withSelector) { - var proxyTags = proxyOutboundList.Where(n => n.tag.StartsWith(Global.ProxyTag)).Select(n => n.tag).ToList(); + var proxyTags = proxyOutboundList.Where(n => n.tag.StartsWith(baseTagName)).Select(n => n.tag).ToList(); if (proxyTags.Count > 1) { proxyOutboundList.InsertRange(0, BuildSelectorOutbounds(proxyTags, baseTagName)); @@ -84,6 +84,8 @@ private void FillOutbound(Outbound4Sbox outbound) try { var protocolExtra = _node.GetProtocolExtra(); + var transportExtra = _node.GetTransportExtra(); + var network = _node.GetNetwork(); outbound.server = _node.Address; outbound.server_port = _node.Port; outbound.type = Global.ProtocolTypes[_node.ConfigType]; @@ -114,27 +116,23 @@ private void FillOutbound(Outbound4Sbox outbound) outbound.password = _node.Password; outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null; - if (_node.Network == nameof(ETransport.tcp) && _node.HeaderType == Global.TcpHeaderHttp) + if (network == nameof(ETransport.raw) && transportExtra.RawHeaderType == Global.RawHeaderHttp) { outbound.plugin = "obfs-local"; - outbound.plugin_opts = $"obfs=http;obfs-host={_node.RequestHost};"; + outbound.plugin_opts = $"obfs=http;obfs-host={transportExtra.Host};"; } else { var pluginArgs = string.Empty; - if (_node.Network == nameof(ETransport.ws)) + if (network == nameof(ETransport.ws)) { pluginArgs += "mode=websocket;"; - pluginArgs += $"host={_node.RequestHost};"; + pluginArgs += $"host={transportExtra.Host};"; // https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172 // Equal signs and commas [and backslashes] must be escaped with a backslash. - var path = _node.Path.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,"); + var path = (transportExtra.Path ?? string.Empty).Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,"); pluginArgs += $"path={path};"; } - else if (_node.Network == nameof(ETransport.quic)) - { - pluginArgs += "mode=quic;"; - } if (_node.StreamSecurity == Global.StreamSecurity) { pluginArgs += "tls;"; @@ -192,15 +190,16 @@ private void FillOutbound(Outbound4Sbox outbound) outbound.packet_encoding = "xudp"; - if (!protocolExtra.Flow.IsNullOrEmpty()) + if (protocolExtra.Flow is "xtls-rprx-vision" or "xtls-rprx-vision-udp443") { - outbound.flow = protocolExtra.Flow; + outbound.flow = "xtls-rprx-vision"; } - else + else if (!protocolExtra.Flow.IsNullOrEmpty()) { - FillOutboundMux(outbound); + outbound.flow = protocolExtra.Flow; } + FillOutboundMux(outbound); FillOutboundTransport(outbound); break; } @@ -229,7 +228,7 @@ private void FillOutbound(Outbound4Sbox outbound) : _config.HysteriaItem.UpMbps; int? downMbps = protocolExtra?.DownMbps is { } sd and >= 0 ? sd - : _config.HysteriaItem.UpMbps; + : _config.HysteriaItem.DownMbps; outbound.up_mbps = upMbps > 0 ? upMbps : null; outbound.down_mbps = downMbps > 0 ? downMbps : null; var ports = protocolExtra?.Ports?.IsNullOrEmpty() == false ? protocolExtra.Ports : null; @@ -311,7 +310,7 @@ private void FillEndpoint(Endpoints4Sbox endpoint) { var protocolExtra = _node.GetProtocolExtra(); - endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress); + endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"]; endpoint.type = Global.ProtocolTypes[_node.ConfigType]; switch (_node.ConfigType) @@ -320,13 +319,12 @@ private void FillEndpoint(Endpoints4Sbox endpoint) { var peer = new Peer4Sbox { - public_key = protocolExtra.WgPublicKey, + public_key = protocolExtra.WgPublicKey ?? string.Empty, pre_shared_key = protocolExtra.WgPresharedKey, - reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(), + reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(), address = _node.Address, port = _node.Port, - // TODO default ["0.0.0.0/0", "::/0"] - allowed_ips = new() { "0.0.0.0/0", "::/0" }, + allowed_ips = ["0.0.0.0/0", "::/0"], }; endpoint.private_key = _node.Password; endpoint.mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(); @@ -345,7 +343,7 @@ private void FillOutboundMux(Outbound4Sbox outbound) { try { - var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; + var muxEnabled = _node.MuxEnabled ?? false; if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty()) { var mux = new Multiplex4Sbox() @@ -381,16 +379,25 @@ private void FillOutboundTls(Outbound4Sbox outbound) { serverName = _node.Sni; } - else if (_node.RequestHost.IsNotEmpty()) + else { - serverName = Utils.String2List(_node.RequestHost)?.First(); + var host = _node.GetNetwork() switch + { + nameof(ETransport.raw) => _node.GetTransportExtra().Host, + nameof(ETransport.ws) => _node.GetTransportExtra().Host, + nameof(ETransport.httpupgrade) => _node.GetTransportExtra().Host, + nameof(ETransport.xhttp) => _node.GetTransportExtra().Host, + nameof(ETransport.grpc) => _node.GetTransportExtra().GrpcAuthority, + _ => null, + }; + serverName = Utils.String2List(host)?.First(); } var tls = new Tls4Sbox() { enabled = true, record_fragment = _config.CoreBasicItem.EnableFragment ? true : null, server_name = serverName, - insecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure), + insecure = _node.GetAllowInsecure(), alpn = _node.GetAlpn(), }; if (_node.Fingerprint.IsNotEmpty()) @@ -438,27 +445,31 @@ private void FillOutboundTransport(Outbound4Sbox outbound) try { var transport = new Transport4Sbox(); + var transportExtra = _node.GetTransportExtra(); + var useragent = _config.CoreBasicItem.DefUserAgent ?? string.Empty; + var useragentValue = Global.RawHttpUserAgentTexts.GetValueOrDefault(useragent, useragent); switch (_node.GetNetwork()) { - case nameof(ETransport.h2): - transport.type = nameof(ETransport.http); - transport.host = _node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(_node.RequestHost); - transport.path = _node.Path.NullIfEmpty(); - break; - - case nameof(ETransport.tcp): //http - if (_node.HeaderType == Global.TcpHeaderHttp) + case nameof(ETransport.raw): //http + if (transportExtra.RawHeaderType == Global.RawHeaderHttp) { transport.type = nameof(ETransport.http); - transport.host = _node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(_node.RequestHost); - transport.path = _node.Path.NullIfEmpty(); + transport.host = transportExtra.Host.IsNullOrEmpty() + ? null + : Utils.String2List(transportExtra.Host); + transport.path = transportExtra.Path.NullIfEmpty(); + if (!useragentValue.IsNullOrEmpty()) + { + transport.headers ??= new(); + transport.headers.UserAgent = useragentValue; + } } break; case nameof(ETransport.ws): transport.type = nameof(ETransport.ws); - var wsPath = _node.Path; + var wsPath = transportExtra.Path; // Parse eh and ed parameters from path using regex if (!wsPath.IsNullOrEmpty()) @@ -487,29 +498,35 @@ private void FillOutboundTransport(Outbound4Sbox outbound) } transport.path = wsPath.NullIfEmpty(); - if (_node.RequestHost.IsNotEmpty()) + if (transportExtra.Host.IsNotEmpty()) { transport.headers = new() { - Host = _node.RequestHost + Host = transportExtra.Host }; } + if (!useragentValue.IsNullOrEmpty()) + { + transport.headers ??= new(); + transport.headers.UserAgent = useragentValue; + } break; case nameof(ETransport.httpupgrade): transport.type = nameof(ETransport.httpupgrade); - transport.path = _node.Path.NullIfEmpty(); - transport.host = _node.RequestHost.NullIfEmpty(); - - break; + transport.path = transportExtra.Path.NullIfEmpty(); + transport.host = transportExtra.Host.NullIfEmpty(); + if (!useragentValue.IsNullOrEmpty()) + { + transport.headers ??= new(); + transport.headers.UserAgent = useragentValue; + } - case nameof(ETransport.quic): - transport.type = nameof(ETransport.quic); break; case nameof(ETransport.grpc): transport.type = nameof(ETransport.grpc); - transport.service_name = _node.Path; + transport.service_name = transportExtra.GrpcServiceName; transport.idle_timeout = _config.GrpcItem.IdleTimeout?.ToString("##s"); transport.ping_timeout = _config.GrpcItem.HealthCheckTimeout?.ToString("##s"); transport.permit_without_stream = _config.GrpcItem.PermitWithoutStream; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 01c32cc21b4..b39312ebd0c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -24,7 +24,7 @@ private void GenRouting() strategy = directDnsStrategy }; - if (_config.TunModeItem.EnableTun) + if (context.IsTunEnabled) { _coreConfig.route.auto_detect_interface = true; @@ -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,8 +102,7 @@ private void GenRouting() _coreConfig.route.rules.Add(new() { port = [53], - network = ["udp"], - action = "hijack-dns" + action = "hijack-dns", }); } @@ -330,11 +335,52 @@ private void GenRoutingUserRule(RulesItem? item) if (item.Ip?.Count > 0) { var countIp = 0; - foreach (var it in item.Ip) + var negativeIpList = item.Ip.Where(it => it.StartsWith('!')).ToList(); + if (negativeIpList.Count > 0) + { + var positiveIpList = item.Ip.Except(negativeIpList).ToList(); + var positiveRule = rule2; + positiveRule = JsonUtils.DeepCopy(rule2); + positiveRule.outbound = null; + positiveRule.action = null; + foreach (var it in positiveIpList) + { + if (ParseV2Address(it, positiveRule)) + { + countIp++; + } + } + var negativeRule = new Rule4Sbox(); + foreach (var it in negativeIpList) + { + // Remove first '!' and trim spaces + var ip = it[1..].Trim(); + if (ParseV2Address(ip, negativeRule)) + { + countIp++; + } + } + negativeRule.invert = true; + rule2 = new Rule4Sbox() + { + outbound = rule2.outbound, + action = rule2.action, + type = "logical", + mode = "or", + rules = [ + positiveRule, + negativeRule + ] + }; + } + else { - if (ParseV2Address(it, rule2)) + foreach (var it in item.Ip) { - countIp++; + if (ParseV2Address(it, rule2)) + { + countIp++; + } } } if (countIp > 0) @@ -407,10 +453,10 @@ private static bool ParseV2Domain(string domain, Rule4Sbox rule) { return false; } - else if (domain.StartsWith("geosite:")) + else if (domain.StartsWith(Global.GeoSitePrefix)) { rule.geosite ??= []; - rule.geosite?.Add(domain.Substring(8)); + rule.geosite?.Add(domain[Global.GeoSitePrefix.Length..]); } else if (domain.StartsWith("regexp:")) { @@ -451,28 +497,18 @@ private static bool ParseV2Address(string address, Rule4Sbox rule) { return false; } - else if (address.Equals("geoip:private")) + else if (address.Equals($"{Global.GeoIPPrefix}private")) { rule.ip_is_private = true; } - else if (address.StartsWith("geoip:")) - { - rule.geoip ??= new(); - rule.geoip?.Add(address.Substring(6)); - } - else if (address.Equals("geoip:!private")) - { - rule.ip_is_private = false; - } - else if (address.StartsWith("geoip:!")) + else if (address.StartsWith(Global.GeoIPPrefix)) { - rule.geoip ??= new(); - rule.geoip?.Add(address.Substring(6)); - rule.invert = true; + rule.geoip ??= []; + rule.geoip?.Add(address[Global.GeoIPPrefix.Length..]); } else { - rule.ip_cidr ??= new(); + rule.ip_cidr ??= []; rule.ip_cidr?.Add(address); } return true; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs index 71e30e45f95..85963bb4a89 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -15,13 +15,6 @@ public RetResult GenerateClientConfigContent() var ret = new RetResult(); try { - if (!context.AppConfig.TunModeItem.EnableLegacyProtect - && context.IsTunEnabled - && context.TunProtectSsPort is > 0 and <= 65535 - && context.ProxyRelaySsPort is > 0 and <= 65535) - { - return GenerateClientProxyRelayConfig(); - } if (_node == null || !_node.IsValid()) { @@ -63,6 +56,13 @@ public RetResult GenerateClientConfigContent() GenStatistic(); + if (_config.CoreBasicItem.EnableFragment) + { + ApplyOutboundFragment(); + } + ApplyOutboundBindInterface(); + ApplyOutboundSendThrough(); + var finalRule = BuildFinalRule(); if (!string.IsNullOrEmpty(finalRule?.balancerTag)) { @@ -162,6 +162,11 @@ public RetResult GenerateClientSpeedtestConfig(List selecteds) listen = Global.Loopback, port = port, protocol = EInboundProtocol.mixed.ToString(), + settings = new Inboundsettings4Ray() + { + udp = true, + auth = "noauth" + }, }; inbound.tag = inbound.protocol + inbound.port.ToString(); _coreConfig.inbounds.Add(inbound); @@ -195,6 +200,12 @@ public RetResult GenerateClientSpeedtestConfig(List selecteds) _coreConfig.routing.rules.Add(rule); } + if (_config.CoreBasicItem.EnableFragment) + { + ApplyOutboundFragment(); + } + ApplyOutboundBindInterface(); + ApplyOutboundSendThrough(); //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); ret.Success = true; ret.Data = JsonUtils.Serialize(_coreConfig); @@ -252,148 +263,25 @@ public RetResult GenerateClientSpeedtestConfig(int port) listen = Global.Loopback, port = port, protocol = EInboundProtocol.mixed.ToString(), + settings = new Inboundsettings4Ray() + { + udp = true, + auth = "noauth" + }, }); _coreConfig.routing.rules.Add(BuildFinalRule()); - ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); - ret.Success = true; - ret.Data = JsonUtils.Serialize(_coreConfig); - return ret; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - } - - public RetResult GenerateClientProxyRelayConfig() - { - var ret = new RetResult(); - try - { - if (_node == null - || !_node.IsValid()) + if (_config.CoreBasicItem.EnableFragment) { - ret.Msg = ResUI.CheckServerSettings; - return ret; + ApplyOutboundFragment(); } - - if (_node.GetNetwork() is nameof(ETransport.quic)) - { - ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}"; - return ret; - } - - var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); - if (result.IsNullOrEmpty()) - { - ret.Msg = ResUI.FailedGetDefaultConfiguration; - return ret; - } - - _coreConfig = JsonUtils.Deserialize(result); - if (_coreConfig == null) - { - ret.Msg = ResUI.FailedGenDefaultConfiguration; - return ret; - } - - GenLog(); - _coreConfig.outbounds.Clear(); - GenOutbounds(); - GenStatistic(); - - var protectNode = new ProfileItem() - { - CoreType = ECoreType.Xray, - ConfigType = EConfigType.Shadowsocks, - Address = Global.Loopback, - Port = context.TunProtectSsPort, - Password = Global.None, - }; - protectNode.SetProtocolExtra(protectNode.GetProtocolExtra() with - { - SsMethod = Global.None, - }); - - const string protectTag = "tun-protect-ss"; - foreach (var outbound in _coreConfig.outbounds - .Where(o => o.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) - { - outbound.streamSettings ??= new(); - outbound.streamSettings.sockopt ??= new(); - outbound.streamSettings.sockopt.dialerProxy = protectTag; - } - // ech protected - foreach (var outbound in _coreConfig.outbounds - .Where(outbound => outbound.streamSettings?.tlsSettings?.echConfigList?.IsNullOrEmpty() == false)) - { - outbound.streamSettings!.tlsSettings!.echSockopt ??= new(); - outbound.streamSettings.tlsSettings.echSockopt.dialerProxy = protectTag; - } - // xhttp download protected - foreach (var outbound in _coreConfig.outbounds - .Where(o => o.streamSettings?.xhttpSettings?.extra is not null)) - { - var xhttpExtra = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings.xhttpSettings!.extra)); - if (xhttpExtra is not JsonObject xhttpExtraObject - || xhttpExtraObject["downloadSettings"] is not JsonObject downloadSettings) - { - continue; - } - // dialerProxy - var sockopt = downloadSettings["sockopt"] as JsonObject ?? new JsonObject(); - sockopt["dialerProxy"] = protectTag; - downloadSettings["sockopt"] = sockopt; - // ech protected - if (downloadSettings["tlsSettings"] is JsonObject tlsSettings - && tlsSettings["echConfigList"] is not null) - { - tlsSettings["echSockopt"] = new JsonObject - { - ["dialerProxy"] = protectTag - }; - } - outbound.streamSettings.xhttpSettings.extra = xhttpExtraObject; - } - _coreConfig.outbounds.Add(new CoreConfigV2rayService(context with - { - Node = protectNode, - }).BuildProxyOutbound(protectTag)); - - _coreConfig.routing.rules ??= []; - var hasBalancer = _coreConfig.routing.balancers is { Count: > 0 }; - _coreConfig.routing.rules.Add(new() - { - inboundTag = ["proxy-relay-ss"], - outboundTag = hasBalancer ? null : Global.ProxyTag, - balancerTag = hasBalancer ? Global.ProxyTag + Global.BalancerTagSuffix : null, - type = "field" - }); - - //_coreConfig.inbounds.Clear(); - - var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!; - configNode["inbounds"]!.AsArray().Add(new - { - listen = Global.Loopback, - port = context.ProxyRelaySsPort, - protocol = "shadowsocks", - settings = new - { - network = "tcp,udp", - method = Global.None, - password = Global.None, - }, - tag = "proxy-relay-ss", - }); + ApplyOutboundBindInterface(); + ApplyOutboundSendThrough(); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Success = true; - ret.Data = JsonUtils.Serialize(configNode); + ret.Data = JsonUtils.Serialize(_coreConfig); return ret; } catch (Exception ex) 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/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs index db7c3f321d4..a218ac132c3 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -5,12 +5,18 @@ public partial class CoreConfigV2rayService private string ApplyFullConfigTemplate() { var fullConfigTemplate = context.FullConfigTemplate; - if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty()) + if (fullConfigTemplate is not { Enabled: true }) { return JsonUtils.Serialize(_coreConfig); } - var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config); + var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; + if (fullConfigTemplateItem.IsNullOrEmpty()) + { + return JsonUtils.Serialize(_coreConfig); + } + + var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); if (fullConfigTemplateNode == null) { return JsonUtils.Serialize(_coreConfig); @@ -127,4 +133,80 @@ private string ApplyFullConfigTemplate() return JsonUtils.Serialize(fullConfigTemplateNode); } + + private void ApplyOutboundBindInterface() + { + var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); + if (bindInterface.IsNullOrEmpty()) + { + return; + } + if (!(context.IsTunEnabled || context.IsWindows)) + { + return; + } + foreach (var outbound in _coreConfig.outbounds ?? []) + { + if (!ShouldBindNet(outbound)) + { + continue; + } + outbound.streamSettings ??= new(); + outbound.streamSettings.sockopt ??= new(); + outbound.streamSettings.sockopt.Interface = bindInterface; + // xhttp download bind interface + if (outbound?.streamSettings?.xhttpSettings?.extra is null) + { + continue; + } + var xhttpExtra = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings.xhttpSettings!.extra)); + if (xhttpExtra is not JsonObject xhttpExtraObject + || xhttpExtraObject["downloadSettings"] is not JsonObject downloadSettings) + { + continue; + } + var sockopt = downloadSettings["sockopt"] as JsonObject ?? new JsonObject(); + sockopt["interface"] = bindInterface; + downloadSettings["sockopt"] = sockopt; + outbound.streamSettings.xhttpSettings.extra = xhttpExtraObject; + } + } + + private void ApplyOutboundSendThrough() + { + var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx(); + if (sendThrough.IsNullOrEmpty()) + { + return; + } + foreach (var outbound in _coreConfig.outbounds ?? []) + { + outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null; + } + } + + private static bool ShouldBindNet(Outbounds4Ray outbound) + { + if (outbound.protocol is "freedom" or "blackhole" or "dns" or "loopback") + { + return false; + } + + if (outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == false) + { + return false; + } + + var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address + ?? outbound.settings?.vnext?.FirstOrDefault()?.address + ?? outbound.settings?.address?.ToString() + ?? string.Empty; + + if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return !IPAddress.TryParse(outboundAddress, out var address) || !IPAddress.IsLoopback(address); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 6b0d335daac..abb3e1348a7 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -121,7 +121,7 @@ private void FillDnsServers(Dns4Ray dnsItem) var proxyGeositeList = new List(); var expectedDomainList = new List(); var expectedIPs = new List(); - var regionNames = new HashSet(); + var regionName = string.Empty; var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.First()); var dnsServerDomains = new List(); @@ -160,18 +160,14 @@ private void FillDnsServers(Dns4Ray dnsItem) .Where(s => !string.IsNullOrEmpty(s)) .ToList(); - foreach (var ip in expectedIPs) + foreach (var region in from ip in expectedIPs + where ip.StartsWith(Global.GeoIPPrefix, StringComparison.OrdinalIgnoreCase) + select ip[Global.GeoIPPrefix.Length..] + into region + where !string.IsNullOrEmpty(region) + select region) { - if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase)) - { - var region = ip["geoip:".Length..]; - if (!string.IsNullOrEmpty(region)) - { - regionNames.Add($"geosite:{region}"); - regionNames.Add($"geosite:geolocation-{region}"); - regionNames.Add($"geosite:tld-{region}"); - } - } + regionName = region; } } @@ -201,9 +197,14 @@ private void FillDnsServers(Dns4Ray dnsItem) if (item.OutboundTag == Global.DirectTag) { - if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:")) + if (normalizedDomain.StartsWith(Global.GeoSitePrefix) || normalizedDomain.StartsWith("ext:")) { - (regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain); + var isExpectedDomain = !regionName.IsNullOrEmpty() + && (normalizedDomain.EndsWith($"-{regionName}") + || normalizedDomain.EndsWith($"@{regionName}") + || normalizedDomain == Global.GeoSitePrefix + regionName); + var targetList = isExpectedDomain ? expectedDomainList : directGeositeList; + targetList.Add(normalizedDomain); } else { @@ -212,7 +213,7 @@ private void FillDnsServers(Dns4Ray dnsItem) } else if (item.OutboundTag != Global.BlockTag) { - if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:")) + if (normalizedDomain.StartsWith(Global.GeoSitePrefix) || normalizedDomain.StartsWith("ext:")) { proxyGeositeList.Add(normalizedDomain); } @@ -370,11 +371,11 @@ private void GenDnsCustom() try { var item = context.RawDnsItem; - var normalDNS = item?.NormalDNS; + var customDNS = context.IsTunEnabled ? item?.TunDNS : item?.NormalDNS; var domainStrategy4Freedom = item?.DomainStrategy4Freedom; - if (normalDNS.IsNullOrEmpty()) + if (customDNS.IsNullOrEmpty()) { - normalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + customDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); } //Outbound Freedom domainStrategy @@ -389,11 +390,11 @@ private void GenDnsCustom() } } - var obj = JsonUtils.ParseJson(normalDNS); + var obj = JsonUtils.ParseJson(customDNS); if (obj is null) { List servers = []; - var arrDNS = normalDNS.Split(','); + var arrDNS = customDNS.Split(','); foreach (var str in arrDNS) { servers.Add(str); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 7ae12c7caf7..7f950a83b32 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -7,36 +7,121 @@ private void GenInbounds() try { var listen = "0.0.0.0"; + var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); _coreConfig.inbounds = []; - var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true); - _coreConfig.inbounds.Add(inbound); + var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort; - if (_config.Inbound.First().SecondLocalPortEnabled) + if (!context.IsTunEnabled || !isUsingLocalMixedPort) { - var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true); - _coreConfig.inbounds.Add(inbound2); - } + _coreConfig.inbounds.Add(inbound); - if (_config.Inbound.First().AllowLANConn) - { - if (_config.Inbound.First().NewPort4LAN) + if (_config.Inbound.First().SecondLocalPortEnabled) + { + var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true); + _coreConfig.inbounds.Add(inbound2); + } + + if (_config.Inbound.First().AllowLANConn) { - var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true); - inbound3.listen = listen; - _coreConfig.inbounds.Add(inbound3); + if (_config.Inbound.First().NewPort4LAN) + { + var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true); + inbound3.listen = listen; + _coreConfig.inbounds.Add(inbound3); + + // auth + if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) + { + inbound3.settings.auth = "password"; + inbound3.settings.accounts = + [ + new() + { + user = _config.Inbound.First().User, + pass = _config.Inbound.First().Pass, + }, - //auth - if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) + ]; + } + } + else { - inbound3.settings.auth = "password"; - inbound3.settings.accounts = new List { new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } }; + inbound.listen = listen; } } - else + } + + if (context.IsTunEnabled) + { + if (_config.TunModeItem.Mtu <= 0) + { + _config.TunModeItem.Mtu = Global.TunMtus.First(); + } + 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) + { + 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()) { - inbound.listen = listen; + 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); } } catch (Exception ex) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 5bf3f1dd3b9..458b86bbca8 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -12,6 +12,10 @@ private void GenOutbounds() GenObservatory(multipleLoad); GenBalancer(multipleLoad); } + if (context.IsTunEnabled) + { + _coreConfig.outbounds.Add(BuildDnsOutbound()); + } } private List BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag) @@ -25,35 +29,6 @@ private List BuildAllProxyOutbounds(string baseTagName = Global.P { proxyOutboundList.Add(BuildProxyOutbound(baseTagName)); } - - if (_config.CoreBasicItem.EnableFragment) - { - var fragmentOutbound = new Outbounds4Ray - { - protocol = "freedom", - tag = $"frag-{baseTagName}", - settings = new() - { - fragment = new() - { - packets = _config.Fragment4RayItem?.Packets, - length = _config.Fragment4RayItem?.Length, - interval = _config.Fragment4RayItem?.Interval - } - } - }; - var actOutboundWithTlsList = - proxyOutboundList.Where(n => n.streamSettings?.security.IsNullOrEmpty() == false - && (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)).ToList(); - if (actOutboundWithTlsList.Count > 0) - { - proxyOutboundList.Add(fragmentOutbound); - } - foreach (var outbound in actOutboundWithTlsList) - { - FillDialerProxy(outbound, fragmentOutbound.tag); - } - } return proxyOutboundList; } @@ -87,7 +62,7 @@ private void FillOutbound(Outbounds4Ray outbound) try { var protocolExtra = _node.GetProtocolExtra(); - var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; + var muxEnabled = _node.MuxEnabled ?? false; switch (_node.ConfigType) { case EConfigType.VMess: @@ -225,12 +200,13 @@ private void FillOutbound(Outbounds4Ray outbound) usersItem.email = Global.UserEMail; usersItem.encryption = protocolExtra.VlessEncryption; - if (!protocolExtra.Flow.IsNullOrEmpty()) + if (protocolExtra.Flow.IsNullOrEmpty()) { - usersItem.flow = protocolExtra.Flow; + FillOutboundMux(outbound, muxEnabled, muxEnabled); } else { + usersItem.flow = protocolExtra.Flow; FillOutboundMux(outbound, false, muxEnabled); } outbound.settings.servers = null; @@ -282,13 +258,14 @@ private void FillOutbound(Outbounds4Ray outbound) var peer = new WireguardPeer4Ray { publicKey = protocolExtra.WgPublicKey ?? "", - endpoint = address + ":" + _node.Port.ToString() + endpoint = address + ":" + _node.Port.ToString(), + preSharedKey = protocolExtra.WgPresharedKey, }; var setting = new Outboundsettings4Ray { - address = Utils.String2List(protocolExtra.WgInterfaceAddress), + address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"], secretKey = _node.Password, - reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(), + reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(), mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(), peers = [peer] }; @@ -348,8 +325,51 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) network = "hysteria"; } streamSettings.network = network; - var host = _node.RequestHost.TrimEx(); - var path = _node.Path.TrimEx(); + var transport = _node.GetTransportExtra(); + var host = string.Empty; + var path = string.Empty; + var kcpSeed = string.Empty; + var kcpMtu = 0; + var headerType = string.Empty; + var xhttpExtra = string.Empty; + switch (network) + { + case nameof(ETransport.raw): + host = transport.Host?.TrimEx() ?? string.Empty; + path = transport.Path?.TrimEx() ?? string.Empty; + headerType = transport.RawHeaderType?.TrimEx() ?? string.Empty; + break; + + case nameof(ETransport.kcp): + kcpSeed = transport.KcpSeed?.TrimEx() ?? string.Empty; + headerType = transport.KcpHeaderType?.TrimEx() ?? string.Empty; + kcpMtu = transport.KcpMtu > 0 ? transport.KcpMtu!.Value : _config.KcpItem.Mtu; + break; + + case nameof(ETransport.ws): + host = transport.Host?.TrimEx() ?? string.Empty; + path = transport.Path?.TrimEx() ?? string.Empty; + break; + + case nameof(ETransport.httpupgrade): + host = transport.Host?.TrimEx() ?? string.Empty; + path = transport.Path?.TrimEx() ?? string.Empty; + break; + + case nameof(ETransport.xhttp): + host = transport.Host?.TrimEx() ?? string.Empty; + path = transport.Path?.TrimEx() ?? string.Empty; + headerType = transport.XhttpMode?.TrimEx() ?? string.Empty; + xhttpExtra = transport.XhttpExtra?.TrimEx() ?? string.Empty; + break; + + case nameof(ETransport.grpc): + host = transport.GrpcAuthority?.TrimEx() ?? string.Empty; + path = transport.GrpcServiceName?.TrimEx() ?? string.Empty; + headerType = transport.GrpcMode?.TrimEx() ?? string.Empty; + break; + } + var sni = _node.Sni.TrimEx(); var useragent = _config.CoreBasicItem.DefUserAgent ?? string.Empty; @@ -360,11 +380,11 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) TlsSettings4Ray tlsSettings = new() { - allowInsecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure), + allowInsecure = _node.GetAllowInsecure(), alpn = _node.GetAlpn(), fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint, echConfigList = _node.EchConfigList.NullIfEmpty(), - echForceQuery = _node.EchForceQuery.NullIfEmpty() + verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(), }; if (sni.IsNotEmpty()) { @@ -374,6 +394,11 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) { tlsSettings.serverName = Utils.String2List(host)?.First(); } + if (!tlsSettings.echConfigList.IsNullOrEmpty()) + { + // For legacy xray compatibility, remove this in the future + tlsSettings.echForceQuery = "full"; + } var certs = CertPemManager.ParsePemChain(_node.Cert); if (certs.Count > 0) { @@ -424,42 +449,41 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) case nameof(ETransport.kcp): KcpSettings4Ray kcpSettings = new() { - mtu = _config.KcpItem.Mtu, + mtu = kcpMtu, tti = _config.KcpItem.Tti }; kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity; kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity; - kcpSettings.congestion = _config.KcpItem.Congestion; - kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; - kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; + kcpSettings.cwndMultiplier = _config.KcpItem.CwndMultiplier; + kcpSettings.maxSendingWindow = _config.KcpItem.MaxSendingWindow; var kcpFinalmask = new Finalmask4Ray(); - if (Global.KcpHeaderMaskMap.TryGetValue(_node.HeaderType, out var header)) + if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header)) { kcpFinalmask.udp = [ new Mask4Ray { - type = header, - settings = _node.HeaderType == "dns" && !host.IsNullOrEmpty() ? new MaskSettings4Ray { domain = host } : null + type = "mkcp-legacy", + settings = new MaskSettings4Ray { header = header }, } ]; } kcpFinalmask.udp ??= []; - if (path.IsNullOrEmpty()) + if (kcpSeed.IsNullOrEmpty()) { kcpFinalmask.udp.Add(new Mask4Ray { - type = "mkcp-original" + type = "mkcp-legacy", }); } else { kcpFinalmask.udp.Add(new Mask4Ray { - type = "mkcp-aes128gcm", - settings = new MaskSettings4Ray { password = path } + type = "mkcp-legacy", + settings = new MaskSettings4Ray { value = kcpSeed }, }); } streamSettings.kcpSettings = kcpSettings; @@ -468,7 +492,6 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) //ws case nameof(ETransport.ws): WsSettings4Ray wsSettings = new(); - wsSettings.headers = new Headers4Ray(); if (host.IsNotEmpty()) { @@ -480,6 +503,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) } if (useragent.IsNotEmpty()) { + wsSettings.headers ??= new Headers4Ray(); wsSettings.headers.UserAgent = useragent; } streamSettings.wsSettings = wsSettings; @@ -499,6 +523,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) } if (useragent.IsNotEmpty()) { + httpupgradeSettings.headers ??= new Headers4Ray(); httpupgradeSettings.headers.UserAgent = useragent; } streamSettings.httpupgradeSettings = httpupgradeSettings; @@ -517,63 +542,26 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) { xhttpSettings.host = host; } - if (_node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(_node.HeaderType)) + if (headerType.IsNotEmpty() && Global.XhttpMode.Contains(headerType)) { - xhttpSettings.mode = _node.HeaderType; + xhttpSettings.mode = headerType; } - if (_node.Extra.IsNotEmpty()) + if (xhttpExtra.IsNotEmpty()) { - xhttpSettings.extra = JsonUtils.ParseJson(_node.Extra); + xhttpSettings.extra = JsonUtils.ParseJson(xhttpExtra); } streamSettings.xhttpSettings = xhttpSettings; FillOutboundMux(outbound); break; - //h2 - case nameof(ETransport.h2): - HttpSettings4Ray httpSettings = new(); - - if (host.IsNotEmpty()) - { - httpSettings.host = Utils.String2List(host); - } - httpSettings.path = path; - - streamSettings.httpSettings = httpSettings; - - break; - //quic - case nameof(ETransport.quic): - QuicSettings4Ray quicsettings = new() - { - security = host, - key = path, - header = new Header4Ray - { - type = _node.HeaderType - } - }; - streamSettings.quicSettings = quicsettings; - if (_node.StreamSecurity == Global.StreamSecurity) - { - if (sni.IsNotEmpty()) - { - streamSettings.tlsSettings.serverName = sni; - } - else - { - streamSettings.tlsSettings.serverName = _node.Address; - } - } - break; case nameof(ETransport.grpc): GrpcSettings4Ray grpcSettings = new() { authority = host.NullIfEmpty(), serviceName = path, - multiMode = _node.HeaderType == Global.GrpcMultiMode, + multiMode = headerType == Global.GrpcMultiMode, idle_timeout = _config.GrpcItem.IdleTimeout, health_check_timeout = _config.GrpcItem.HealthCheckTimeout, permit_without_stream = _config.GrpcItem.PermitWithoutStream, @@ -591,7 +579,7 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) : _config.HysteriaItem.UpMbps; int? downMbps = protocolExtra?.DownMbps is { } sd and >= 0 ? sd - : _config.HysteriaItem.UpMbps; + : _config.HysteriaItem.DownMbps; var hopInterval = !protocolExtra.HopInterval.IsNullOrEmpty() ? protocolExtra.HopInterval : (_config.HysteriaItem.HopInterval >= 5 @@ -640,20 +628,20 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) break; default: - //tcp - if (_node.HeaderType == Global.TcpHeaderHttp) + // raw + if (headerType == Global.RawHeaderHttp) { - TcpSettings4Ray tcpSettings = new() + RawSettings4Ray rawSettings = new() { header = new Header4Ray { - type = _node.HeaderType + type = headerType } }; //request Host var request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); - var useragentValue = Global.TcpHttpUserAgentTexts.GetValueOrDefault(useragent, useragent); + var useragentValue = Global.RawHttpUserAgentTexts.GetValueOrDefault(useragent, useragent); var arrHost = host.Split(','); var host2 = string.Join(",".AppendQuotes(), arrHost); request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); @@ -666,9 +654,9 @@ private void FillBoundStreamSettings(Outbounds4Ray outbound) pathHttp = string.Join(",".AppendQuotes(), arrPath); } request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); - tcpSettings.header.request = JsonUtils.Deserialize(request); + rawSettings.header.request = JsonUtils.Deserialize(request); - streamSettings.tcpSettings = tcpSettings; + streamSettings.rawSettings = rawSettings; } break; } @@ -824,4 +812,74 @@ private static void FillDialerProxy(Outbounds4Ray outbound, string dialerProxyTa } } } + + private static Outbounds4Ray BuildDnsOutbound() + { + var outbound = new Outbounds4Ray { tag = Global.DnsOutboundTag, protocol = "dns", }; + return outbound; + } + + private void ApplyOutboundFragment() + { + var actOutboundWithTlsList = + _coreConfig.outbounds.Where(n => n.streamSettings?.security.IsNullOrEmpty() == false + && (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) + .ToList(); + + var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello"; + var configLength = _config.Fragment4RayItem?.Length ?? "50-100"; + var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20"; + + var fragmentMask = new Mask4Ray + { + type = "fragment", + settings = new MaskSettings4Ray + { + packets = configPackets, + length = configLength, + delay = configDelay, + } + }; + var noiseMask = new Mask4Ray + { + type = "noise", + settings = new MaskSettings4Ray + { + length = "10-20", + delay = "10-16", + } + }; + + foreach (var outbound in actOutboundWithTlsList) + { + //var packets = configPackets; + //if (outbound.streamSettings.security == Global.StreamSecurityReality + // && packets == "tlshello") + //{ + // packets = "1-3"; + //} + //else if (outbound.streamSettings.security == Global.StreamSecurity + // && packets != "tlshello") + //{ + // packets = "tlshello"; + //} + var finalMaskJsonObj = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings?.finalmask)) as JsonObject ?? new JsonObject(); + // tcp fragment + var tcpFinalmaskList = finalMaskJsonObj["tcp"] as JsonArray ?? []; + if (tcpFinalmaskList.Count == 0) + { + tcpFinalmaskList.Add(JsonUtils.SerializeToNode(fragmentMask)); + finalMaskJsonObj["tcp"] = tcpFinalmaskList; + } + // udp noise + var udpFinalmaskList = finalMaskJsonObj["udp"] as JsonArray ?? []; + if (udpFinalmaskList.Count == 0) + { + udpFinalmaskList.Add(JsonUtils.SerializeToNode(noiseMask)); + finalMaskJsonObj["udp"] = udpFinalmaskList; + } + // write back + outbound.streamSettings.finalmask = finalMaskJsonObj; + } + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs index dd752a682a8..c3bcf61643c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -6,6 +6,32 @@ private void GenRouting() { try { + if (context.IsTunEnabled) + { + var tunRules = JsonUtils.Deserialize>(EmbedUtils.GetEmbedText(Global.V2raySampleTunRules)); + if (tunRules != null) + { + _coreConfig.routing.rules.AddRange(tunRules); + } + var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe(); + _coreConfig.routing.rules.Add(new() + { + port = "53", + process = lstDnsExe, + outboundTag = Global.DnsOutboundTag, + }); + _coreConfig.routing.rules.Add(new() + { + process = lstDirectExe, + outboundTag = Global.DirectTag, + }); + _coreConfig.routing.rules.Add(new() + { + inboundTag = ["tun"], + port = "53", + outboundTag = Global.DnsOutboundTag, + }); + } if (_coreConfig.routing?.rules != null) { _coreConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy; @@ -205,4 +231,37 @@ private RulesItem4Ray BuildFinalRule() } return finalRule; } + + private static (List lstDnsExe, List lstDirectExe) BuildRoutingDirectExe() + { + var dnsExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var directExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); + + foreach (var coreConfig in coreInfoResult) + { + if (coreConfig.CoreType == ECoreType.v2rayN) + { + continue; + } + + foreach (var baseExeName in coreConfig.CoreExes) + { + if (coreConfig.CoreType != ECoreType.Xray) + { + dnsExeSet.Add(Utils.GetExeName(baseExeName)); + } + directExeSet.Add(Utils.GetExeName(baseExeName)); + } + } + + directExeSet.Add("xray/"); + directExeSet.Add("self/"); + + var lstDnsExe = new List(dnsExeSet); + var lstDirectExe = new List(directExeSet); + + return (lstDnsExe, lstDirectExe); + } } 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 362f64d0b1c..11778b22016 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -1,3 +1,5 @@ +using ServiceLib.UdpTest; + namespace ServiceLib.Services; public class SpeedtestService(Config config, Func updateFunc) @@ -6,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) { @@ -49,6 +53,10 @@ private async Task RunAsync(ESpeedActionType actionType, List selec await RunRealPingBatchAsync(lstSelected, exitLoopKey); break; + case ESpeedActionType.UdpTest: + await RunUdpTestBatchAsync(lstSelected, exitLoopKey); + break; + case ESpeedActionType.Speedtest: await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey); break; @@ -101,6 +109,7 @@ private async Task> GetClearItem(ESpeedActionType actionTyp { case ESpeedActionType.Tcping: case ESpeedActionType.Realping: + case ESpeedActionType.UdpTest: await UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); break; @@ -128,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); @@ -165,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 @@ -238,6 +254,86 @@ private async Task RunRealPingAsync(List selecteds, string return true; } + private async Task RunUdpTestBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0) + { + if (pageSize <= 0) + { + pageSize = Math.Min(lstSelected.Count, _speedTestPageSize); + } + var lstTest = GetTestBatchItem(lstSelected, pageSize); + + List lstFailed = new(); + foreach (var lst in lstTest) + { + var ret = await RunUdpTestAsync(lst, exitLoopKey); + if (ret == false) + { + lstFailed.AddRange(lst); + } + await Task.Delay(_delayInterval); + } + + //Retest the failed part + if (lstFailed.Count > 0) + { + if (ShouldStopTest(exitLoopKey)) + { + await UpdateFunc("", ResUI.SpeedtestingSkip); + return; + } + + await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); + + await RunUdpTestAsync(lstFailed, exitLoopKey); + } + } + + private async Task RunUdpTestAsync(List selecteds, string exitLoopKey) + { + ProcessService processService = null; + try + { + processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); + if (processService is null) + { + return false; + } + await Task.Delay(1000); + + List tasks = new(); + foreach (var it in selecteds) + { + if (!it.AllowTest) + { + continue; + } + + if (ShouldStopTest(exitLoopKey)) + { + return false; + } + + tasks.Add(Task.Run(async () => + { + await DoUdpTest(it); + })); + } + await Task.WhenAll(tasks); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + finally + { + if (processService != null) + { + await processService?.StopAsync(); + } + } + return true; + } + private async Task RunMixedTestAsync(List selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey) { using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount); @@ -305,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; } @@ -330,6 +439,24 @@ await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, m }); } + private async Task DoUdpTest(ServerTestItem it) + { + var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl); + var responseTime = -1; + try + { + responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds; + } + catch + { + // ignored + } + + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); + await UpdateFunc(it.IndexId, responseTime.ToString()); + return responseTime; + } + private async Task GetTcpingTime(string url, int port) { var responseTime = -1; @@ -386,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 f9d9a5f8e58..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(); @@ -304,6 +341,8 @@ private async Task ParseDownloadUrl(ECoreType type, UpdateResult r return RuntimeInformation.ProcessArchitecture switch { Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64, + Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64, + Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64, Architecture.X64 => coreInfo?.DownloadUrlLinux64, _ => null, }; @@ -370,8 +409,8 @@ private async Task UpdateSrsFileAll() var rules = JsonUtils.Deserialize>(routing.RuleSet); foreach (var item in rules ?? []) { - AddPrefixedItems(item.Ip, "geoip:", geoipFiles); - AddPrefixedItems(item.Domain, "geosite:", geoSiteFiles); + AddPrefixedItems(item.Ip, Global.GeoIPPrefix, geoipFiles); + AddPrefixedItems(item.Domain, Global.GeoSitePrefix, geoSiteFiles); } } 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 e5c307c8e06..d25f5b5f563 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -8,6 +8,12 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string? CoreType { get; set; } + [Reactive] + public bool AllowInsecure { get; set; } + + [Reactive] + public bool MuxEnabled { get; set; } + [Reactive] public string Cert { get; set; } @@ -17,6 +23,9 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string CertSha { get; set; } + [Reactive] + public bool AllowInsecureCertFetch { get; set; } + [Reactive] public string SalamanderPass { get; set; } @@ -50,8 +59,9 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string WgPublicKey { get; set; } - //[Reactive] - //public string WgPresharedKey { get; set; } + [Reactive] + public string WgPresharedKey { get; set; } + [Reactive] public string WgInterfaceAddress { get; set; } @@ -73,6 +83,151 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public bool NaiveQuic { get; set; } + [Reactive] + public string RawHeaderType { get; set; } + + [Reactive] + public string Host { get; set; } + + [Reactive] + public string Path { get; set; } + + [Reactive] + public string XhttpMode { get; set; } + + [Reactive] + public string XhttpExtra { get; set; } + + [Reactive] + public string GrpcAuthority { get; set; } + + [Reactive] + public string GrpcServiceName { get; set; } + + [Reactive] + public string GrpcMode { get; set; } + + [Reactive] + public string KcpHeaderType { get; set; } + + [Reactive] + public string KcpSeed { get; set; } + + [Reactive] + public int? KcpMtu { get; set; } + + public string TransportHeaderType + { + get => SelectedSource.GetNetwork() switch + { + nameof(ETransport.raw) => RawHeaderType, + nameof(ETransport.kcp) => KcpHeaderType, + nameof(ETransport.xhttp) => XhttpMode, + nameof(ETransport.grpc) => GrpcMode, + _ => string.Empty, + }; + set + { + switch (SelectedSource.GetNetwork()) + { + case nameof(ETransport.raw): + RawHeaderType = value; + break; + + case nameof(ETransport.kcp): + KcpHeaderType = value; + break; + + case nameof(ETransport.xhttp): + XhttpMode = value; + break; + + case nameof(ETransport.grpc): + GrpcMode = value; + break; + } + this.RaisePropertyChanged(); + } + } + + public string TransportHost + { + get => SelectedSource.GetNetwork() switch + { + nameof(ETransport.raw) => Host, + nameof(ETransport.ws) => Host, + nameof(ETransport.httpupgrade) => Host, + nameof(ETransport.xhttp) => Host, + nameof(ETransport.grpc) => GrpcAuthority, + _ => string.Empty, + }; + set + { + switch (SelectedSource.GetNetwork()) + { + case nameof(ETransport.raw): + case nameof(ETransport.ws): + case nameof(ETransport.httpupgrade): + case nameof(ETransport.xhttp): + Host = value; + break; + + case nameof(ETransport.grpc): + GrpcAuthority = value; + break; + } + this.RaisePropertyChanged(); + } + } + + public string TransportPath + { + get => SelectedSource.GetNetwork() switch + { + nameof(ETransport.kcp) => KcpSeed, + nameof(ETransport.ws) => Path, + nameof(ETransport.httpupgrade) => Path, + nameof(ETransport.xhttp) => Path, + nameof(ETransport.grpc) => GrpcServiceName, + _ => string.Empty, + }; + set + { + switch (SelectedSource.GetNetwork()) + { + case nameof(ETransport.kcp): + KcpSeed = value; + break; + + case nameof(ETransport.ws): + case nameof(ETransport.httpupgrade): + case nameof(ETransport.xhttp): + Path = value; + break; + + case nameof(ETransport.grpc): + GrpcServiceName = value; + break; + } + this.RaisePropertyChanged(); + } + } + + public string TransportExtraText + { + get => SelectedSource.GetNetwork() == nameof(ETransport.xhttp) + ? XhttpExtra + : string.Empty; + set + { + if (SelectedSource.GetNetwork() == nameof(ETransport.xhttp)) + { + XhttpExtra = value; + } + this.RaisePropertyChanged(); + } + } + public ReactiveCommand FetchCertCmd { get; } public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } @@ -101,14 +256,21 @@ public AddServerViewModel(ProfileItem profileItem, Func x.CertSha) .Subscribe(_ => UpdateCertTip()); + this.WhenAnyValue(x => x.SelectedSource.Network) + .Subscribe(_ => + { + this.RaisePropertyChanged(nameof(TransportHeaderType)); + this.RaisePropertyChanged(nameof(TransportHost)); + this.RaisePropertyChanged(nameof(TransportPath)); + this.RaisePropertyChanged(nameof(TransportExtraText)); + }); + this.WhenAnyValue(x => x.Cert) .Subscribe(_ => UpdateCertSha()); if (profileItem.IndexId.IsNullOrEmpty()) { profileItem.Network = Global.DefaultNetwork; - profileItem.HeaderType = Global.None; - profileItem.RequestHost = ""; profileItem.StreamSecurity = ""; SelectedSource = profileItem; } @@ -117,28 +279,44 @@ public AddServerViewModel(ProfileItem profileItem, Func 0 ? protocolExtra.InsecureConcurrency : null; - NaiveQuic = protocolExtra?.NaiveQuic ?? false; + AllowInsecure = SelectedSource?.GetAllowInsecure() == true; + MuxEnabled = SelectedSource?.MuxEnabled == true; + Cert = SelectedSource?.Cert ?? string.Empty; + CertSha = SelectedSource?.CertSha ?? string.Empty; + + var protocolExtra = SelectedSource?.GetProtocolExtra() ?? new(); + var transport = SelectedSource?.GetTransportExtra() ?? new(); + Ports = protocolExtra.Ports ?? string.Empty; + AlterId = int.TryParse(protocolExtra.AlterId, out var result) ? result : 0; + Flow = protocolExtra.Flow ?? string.Empty; + SalamanderPass = protocolExtra.SalamanderPass ?? string.Empty; + UpMbps = protocolExtra.UpMbps; + DownMbps = protocolExtra.DownMbps; + HopInterval = protocolExtra.HopInterval ?? string.Empty; + VmessSecurity = protocolExtra.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity; + VlessEncryption = protocolExtra.VlessEncryption?.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None; + SsMethod = protocolExtra.SsMethod ?? string.Empty; + WgPublicKey = protocolExtra.WgPublicKey ?? string.Empty; + WgPresharedKey = protocolExtra.WgPresharedKey ?? string.Empty; + WgInterfaceAddress = protocolExtra.WgInterfaceAddress ?? string.Empty; + WgReserved = protocolExtra.WgReserved ?? string.Empty; + WgMtu = protocolExtra.WgMtu ?? 1280; + Uot = protocolExtra.Uot ?? false; + CongestionControl = protocolExtra.CongestionControl ?? string.Empty; + InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null; + NaiveQuic = protocolExtra.NaiveQuic ?? false; + + RawHeaderType = transport.RawHeaderType ?? Global.None; + Host = transport.Host ?? string.Empty; + Path = transport.Path ?? string.Empty; + XhttpMode = transport.XhttpMode ?? Global.DefaultXhttpMode; + XhttpExtra = transport.XhttpExtra ?? string.Empty; + GrpcAuthority = transport.GrpcAuthority ?? string.Empty; + GrpcServiceName = transport.GrpcServiceName ?? string.Empty; + GrpcMode = transport.GrpcMode.IsNullOrEmpty() ? Global.GrpcGunMode : transport.GrpcMode; + KcpHeaderType = transport.KcpHeaderType.IsNullOrEmpty() ? Global.None : transport.KcpHeaderType; + KcpSeed = transport.KcpSeed ?? string.Empty; + KcpMtu = transport.KcpMtu; } private async Task SaveServerAsync() @@ -182,9 +360,31 @@ private async Task SaveServerAsync() return; } } - SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse(CoreType); + SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse; + SelectedSource.MuxEnabled = MuxEnabled; SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert; SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha; + if (!Global.Networks.Contains(SelectedSource.Network)) + { + SelectedSource.Network = Global.DefaultNetwork; + } + + var transport = new TransportExtraItem + { + RawHeaderType = RawHeaderType.NullIfEmpty(), + Host = Host.NullIfEmpty(), + Path = Path.NullIfEmpty(), + XhttpMode = XhttpMode.NullIfEmpty(), + XhttpExtra = XhttpExtra.NullIfEmpty(), + GrpcAuthority = GrpcAuthority.NullIfEmpty(), + GrpcServiceName = GrpcServiceName.NullIfEmpty(), + GrpcMode = GrpcMode.NullIfEmpty(), + KcpHeaderType = KcpHeaderType.NullIfEmpty(), + KcpSeed = KcpSeed.NullIfEmpty(), + KcpMtu = KcpMtu > 0 ? KcpMtu : null, + }; + SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with { Ports = Ports.NullIfEmpty(), @@ -198,6 +398,7 @@ private async Task SaveServerAsync() VlessEncryption = VlessEncryption.NullIfEmpty(), SsMethod = SsMethod.NullIfEmpty(), WgPublicKey = WgPublicKey.NullIfEmpty(), + WgPresharedKey = WgPresharedKey.NullIfEmpty(), WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(), WgReserved = WgReserved.NullIfEmpty(), WgMtu = WgMtu >= 576 ? WgMtu : null, @@ -206,6 +407,7 @@ private async Task SaveServerAsync() InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null, NaiveQuic = NaiveQuic ? true : null, }); + SelectedSource.SetTransportExtra(transport); if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { @@ -261,7 +463,7 @@ private async Task FetchCert() var serverName = SelectedSource.Sni; if (serverName.IsNullOrEmpty()) { - serverName = SelectedSource.RequestHost; + serverName = GetCurrentTransportHost(); } if (serverName.IsNullOrEmpty()) { @@ -272,7 +474,9 @@ private async Task FetchCert() domain += $":{SelectedSource.Port}"; } - (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); + (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName, + verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), + allowInsecure: AllowInsecureCertFetch); UpdateCertTip(certError); } @@ -286,7 +490,7 @@ private async Task FetchCertChain() var serverName = SelectedSource.Sni; if (serverName.IsNullOrEmpty()) { - serverName = SelectedSource.RequestHost; + serverName = GetCurrentTransportHost(); } if (serverName.IsNullOrEmpty()) { @@ -297,8 +501,23 @@ private async Task FetchCertChain() domain += $":{SelectedSource.Port}"; } - var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); + var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName, + verifyPeerCertByName: Utils.String2List(SelectedSource.VerifyPeerCertByName), + allowInsecure: AllowInsecureCertFetch); Cert = CertPemManager.ConcatenatePemChain(certs); UpdateCertTip(certError); } + + private string GetCurrentTransportHost() + { + return SelectedSource.GetNetwork() switch + { + nameof(ETransport.raw) => Host, + nameof(ETransport.ws) => Host, + nameof(ETransport.httpupgrade) => Host, + nameof(ETransport.xhttp) => Host, + nameof(ETransport.grpc) => GrpcAuthority, + _ => string.Empty, + }; + } } 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/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index 00178d17978..13571616c95 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -20,6 +20,7 @@ public class DNSSettingViewModel : MyReactiveObject [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } [Reactive] public string DomainDNSAddressCompatible { get; set; } [Reactive] public string NormalDNSCompatible { get; set; } + [Reactive] public string TunDNSCompatible { get; set; } [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } [Reactive] public string DomainDNSAddress2Compatible { get; set; } @@ -43,6 +44,7 @@ public DNSSettingViewModel(Func>? updateView) ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => { NormalDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + TunDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); await Task.CompletedTask; }); @@ -84,6 +86,7 @@ private async Task Init() DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty; DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty; NormalDNSCompatible = item1?.NormalDNS ?? string.Empty; + TunDNSCompatible = item1?.TunDNS ?? string.Empty; var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); SBCustomDNSEnableCompatible = item2.Enabled; @@ -124,10 +127,27 @@ private async Task SaveSettingAsync() } } } + if (TunDNSCompatible.IsNotEmpty()) + { + var obj = JsonUtils.ParseJson(TunDNSCompatible); + if (obj != null && obj["servers"] != null) + { + } + else + { + if (TunDNSCompatible.Contains('{') || TunDNSCompatible.Contains('}')) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); + return; + } + } + } if (NormalDNS2Compatible.IsNotEmpty()) { var obj2 = JsonUtils.Deserialize(NormalDNS2Compatible); - if (obj2 == null) + if (obj2 == null + || obj2.servers.Count == 0 + || obj2.servers.Any(s => s.type.IsNullOrEmpty())) { NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); return; @@ -136,7 +156,9 @@ private async Task SaveSettingAsync() if (TunDNS2Compatible.IsNotEmpty()) { var obj2 = JsonUtils.Deserialize(TunDNS2Compatible); - if (obj2 == null) + if (obj2 == null + || obj2.servers.Count == 0 + || obj2.servers.Any(s => s.type.IsNullOrEmpty())) { NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); return; @@ -149,14 +171,15 @@ private async Task SaveSettingAsync() item1.DomainDNSAddress = DomainDNSAddressCompatible; item1.UseSystemHosts = UseSystemHostsCompatible; item1.NormalDNS = NormalDNSCompatible; + item1.TunDNS = TunDNSCompatible; await ConfigHandler.SaveDNSItems(_config, item1); var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); item2.Enabled = SBCustomDNSEnableCompatible; item2.DomainStrategy4Freedom = DomainStrategy4Freedom2Compatible; item2.DomainDNSAddress = DomainDNSAddress2Compatible; - item2.NormalDNS = JsonUtils.Serialize(JsonUtils.ParseJson(NormalDNS2Compatible)); - item2.TunDNS = JsonUtils.Serialize(JsonUtils.ParseJson(TunDNS2Compatible)); + item2.NormalDNS = JsonUtils.Serialize(JsonUtils.Deserialize(NormalDNS2Compatible)); + item2.TunDNS = JsonUtils.Serialize(JsonUtils.Deserialize(TunDNS2Compatible)); await ConfigHandler.SaveDNSItems(_config, item2); await ConfigHandler.SaveConfig(_config); diff --git a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs index 3a50b52ede1..78cd10a2603 100644 --- a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -13,6 +13,9 @@ public class FullConfigTemplateViewModel : MyReactiveObject [Reactive] public string FullConfigTemplate4Ray { get; set; } + [Reactive] + public string FullTunConfigTemplate4Ray { get; set; } + [Reactive] public string FullConfigTemplate4Singbox { get; set; } @@ -50,10 +53,15 @@ public FullConfigTemplateViewModel(Func>? updat private async Task Init() { var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); - EnableFullConfigTemplate4Ray = item?.Enabled ?? false; - FullConfigTemplate4Ray = item?.Config ?? string.Empty; - AddProxyOnly4Ray = item?.AddProxyOnly ?? false; - ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty; + if (item == null) + { + return; + } + EnableFullConfigTemplate4Ray = item.Enabled; + FullConfigTemplate4Ray = item.Config ?? string.Empty; + FullTunConfigTemplate4Ray = item.TunConfig ?? string.Empty; + AddProxyOnly4Ray = item.AddProxyOnly ?? false; + ProxyDetour4Ray = item.ProxyDetour ?? string.Empty; var item2 = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false; @@ -82,10 +90,13 @@ private async Task SaveSettingAsync() private async Task SaveXrayConfigAsync() { var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + if (item == null) + { + return false; + } item.Enabled = EnableFullConfigTemplate4Ray; - item.Config = null; - item.Config = FullConfigTemplate4Ray; + item.TunConfig = FullTunConfigTemplate4Ray; item.AddProxyOnly = AddProxyOnly4Ray; item.ProxyDetour = ProxyDetour4Ray; @@ -97,10 +108,11 @@ private async Task SaveXrayConfigAsync() private async Task SaveSingboxConfigAsync() { var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + if (item == null) + { + return false; + } item.Enabled = EnableFullConfigTemplate4Singbox; - item.Config = null; - item.TunConfig = null; - item.Config = FullConfigTemplate4Singbox; item.TunConfig = FullTunConfigTemplate4Singbox; diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 36027b79d45..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(); @@ -559,7 +578,7 @@ public async Task Reload() await Task.Run(async () => { - await LoadCore(allResult.ResolvedMainContext, allResult.PreSocksResult?.Context); + await LoadCore(allResult.MainResult.Context, allResult.PreSocksResult?.Context); await SysProxyHandler.UpdateSysProxy(_config, false); await Task.Delay(1000); }); diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 8b677ffd686..85af3d02e5e 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -4,27 +4,27 @@ 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 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 LogEnabled { get; set; } + [Reactive] public string Loglevel { 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 @@ -58,6 +58,7 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public int SpeedTestTimeout { get; set; } [Reactive] public string SpeedTestUrl { get; set; } [Reactive] public string SpeedPingTestUrl { get; set; } + [Reactive] public string UdpTestTarget { get; set; } [Reactive] public int MixedConcurrencyCount { get; set; } [Reactive] public bool EnableHWA { get; set; } [Reactive] public string SubConvertUrl { get; set; } @@ -80,9 +81,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; } @@ -97,6 +98,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 @@ -139,26 +141,26 @@ 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; - 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; + LogEnabled = _config.CoreBasicItem.LogEnabled; + Loglevel = _config.CoreBasicItem.Loglevel; + 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 @@ -193,6 +195,7 @@ private async Task Init() SpeedTestUrl = _config.SpeedTestItem.SpeedTestUrl; MixedConcurrencyCount = _config.SpeedTestItem.MixedConcurrencyCount; SpeedPingTestUrl = _config.SpeedTestItem.SpeedPingTestUrl; + UdpTestTarget = _config.SpeedTestItem.UdpTestTarget; EnableHWA = _config.GuiItem.EnableHWA; SubConvertUrl = _config.ConstItem.SubConvertUrl; MainGirdOrientation = (int)_config.UiItem.MainGirdOrientation; @@ -205,9 +208,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; @@ -222,6 +225,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 @@ -291,12 +295,18 @@ 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(); + if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue)) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4); + return; + } var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed || EnableDragDropSort != _config.UiItem.EnableDragDropSort @@ -316,31 +326,31 @@ 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.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.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; @@ -359,6 +369,7 @@ private async Task SaveSettingAsync() _config.SpeedTestItem.MixedConcurrencyCount = MixedConcurrencyCount; _config.SpeedTestItem.SpeedTestUrl = SpeedTestUrl; _config.SpeedTestItem.SpeedPingTestUrl = SpeedPingTestUrl; + _config.SpeedTestItem.UdpTestTarget = UdpTestTarget; _config.GuiItem.EnableHWA = EnableHWA; _config.ConstItem.SubConvertUrl = SubConvertUrl; _config.UiItem.MainGirdOrientation = (EGirdOrientation)MainGirdOrientation; @@ -368,9 +379,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; @@ -382,6 +393,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/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs index b3810b522fb..a4d9b29897f 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -190,7 +190,7 @@ public async Task RefreshSubscriptions() } SelectedSub = (_config.SubIndexId.IsNotEmpty() ? SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) - : null) ?? SubItems.LastOrDefault(); + : null) ?? SubItems.FirstOrDefault(); } private async Task?> GetProfileItemsEx(string subid, string filter) diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 6e7e183ccd4..1af0ac14724 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -60,6 +60,7 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand TcpingServerCmd { get; } public ReactiveCommand RealPingServerCmd { get; } + public ReactiveCommand UdpTestServerCmd { get; } public ReactiveCommand SpeedServerCmd { get; } public ReactiveCommand SortServerResultCmd { get; } public ReactiveCommand RemoveInvalidServerResultCmd { get; } @@ -71,6 +72,7 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand Export2ClientConfigClipboardCmd { get; } public ReactiveCommand Export2ShareUrlCmd { get; } public ReactiveCommand Export2ShareUrlBase64Cmd { get; } + public ReactiveCommand Export2InnerUriCmd { get; } public ReactiveCommand AddSubCmd { get; } public ReactiveCommand EditSubCmd { get; } @@ -178,6 +180,10 @@ public ProfilesViewModel(Func>? updateView) { await ServerSpeedtest(ESpeedActionType.Realping); }, canEditRemove); + UdpTestServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await ServerSpeedtest(ESpeedActionType.UdpTest); + }, canEditRemove); SpeedServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.Speedtest); @@ -207,6 +213,10 @@ public ProfilesViewModel(Func>? updateView) { await Export2ShareUrlAsync(true); }, canEditRemove); + Export2InnerUriCmd = ReactiveCommand.CreateFromTask(async () => + { + await Export2InnerUrlAsync(); + }, canEditRemove); //Subscription AddSubCmd = ReactiveCommand.CreateFromTask(async () => @@ -293,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; } @@ -393,7 +407,7 @@ private async Task RefreshSubscriptions() } SelectedSub = (_config.SubIndexId.IsNotEmpty() ? SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) - : null) ?? SubItems.LastOrDefault(); + : null) ?? SubItems.FirstOrDefault(); } private async Task?> GetProfileItemsEx(string subid, string filter) @@ -427,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), @@ -709,18 +724,22 @@ public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem) public async Task ServerSpeedtest(ESpeedActionType actionType) { - if (actionType == ESpeedActionType.Mixedtest) + List? lstSelected; + if (actionType is ESpeedActionType.Mixedtest or ESpeedActionType.FastRealping) { - SelectedProfiles = ProfileItems; + if (actionType == ESpeedActionType.FastRealping) + { + actionType = ESpeedActionType.Realping; + } + + lstSelected = JsonUtils.Deserialize>(JsonUtils.Serialize(ProfileItems?.OrderBy(t => t.Sort))); } - else if (actionType == ESpeedActionType.FastRealping) + else { - SelectedProfiles = ProfileItems; - actionType = ESpeedActionType.Realping; + lstSelected = await GetProfileItems(false); } - var lstSelected = await GetProfileItems(false); - if (lstSelected == null) + if (lstSelected is null || lstSelected.Count <= 0) { return; } @@ -831,6 +850,32 @@ public async Task Export2ShareUrlAsync(bool blEncode) } } + public async Task Export2InnerUrlAsync() + { + var lstSelected = await GetProfileItems(true); + if (lstSelected == null) + { + return; + } + + var result = string.Empty; + + await Task.Run(() => + { + result = InnerFmt.ToUri(lstSelected); + }); + + if (!result.IsNullOrEmpty()) + { + await _updateView?.Invoke(EViewAction.SetClipboardData, result); + NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + #endregion Add Servers #region Subscription diff --git a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs index 8f62f2d0288..2075bb1e982 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs @@ -22,7 +22,6 @@ public class RoutingSettingViewModel : MyReactiveObject public ReactiveCommand RoutingAdvancedSetDefaultCmd { get; } public ReactiveCommand RoutingAdvancedImportRulesCmd { get; } - public ReactiveCommand SaveCmd { get; } public bool IsModified { get; set; } #endregion Reactive @@ -53,12 +52,19 @@ public RoutingSettingViewModel(Func>? updateVie await RoutingAdvancedImportRules(); }); - SaveCmd = ReactiveCommand.CreateFromTask(async () => - { - await SaveRoutingAsync(); - }); - _ = Init(); + + // Auto-save DomainStrategy when changed + this.WhenAnyValue( + x => x.DomainStrategy, + x => x.DomainStrategy4Singbox) + .Skip(1) + .DistinctUntilChanged() + .Subscribe(x => + { + IsModified = true; + _ = SaveSettingsAsync(); + }); } private async Task Init() @@ -96,20 +102,14 @@ public async Task RefreshRoutingItems() } } - private async Task SaveRoutingAsync() + /// + /// Save DomainStrategy settings + /// + public async Task SaveSettingsAsync() { _config.RoutingBasicItem.DomainStrategy = DomainStrategy; _config.RoutingBasicItem.DomainStrategy4Singbox = DomainStrategy4Singbox; - - if (await ConfigHandler.SaveConfig(_config) == 0) - { - NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); - } - else - { - NoticeManager.Instance.Enqueue(ResUI.OperationFailed); - } + await ConfigHandler.SaveConfig(_config); } #endregion Refresh Save diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index b8154c1647b..0ce40dbb539 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -490,6 +490,7 @@ private async Task DoEnableTun(bool c) } } } + await ConfigHandler.SaveConfig(_config); AppEvents.ReloadRequested.Publish(); } 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/ButtonResult.cs b/v2rayN/v2rayN.Desktop/Common/ButtonResult.cs new file mode 100644 index 00000000000..6f939d8db2b --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/ButtonResult.cs @@ -0,0 +1,8 @@ +namespace v2rayN.Desktop.Common; + +public enum ButtonResult +{ + None = 0, + Yes = 1, + No = 2 +} 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 new file mode 100644 index 00000000000..ba416e719f1 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/QRCodeAvaloniaUtils.cs @@ -0,0 +1,190 @@ +using System.Runtime.InteropServices; +using SkiaSharp; + +namespace v2rayN.Desktop.Common; + +public partial class QRCodeAvaloniaUtils +{ + public static byte[]? CaptureScreen() + { + if (!Utils.IsWindows()) + { + return null; + } + + try + { + return CaptureScreenWindows(); + } + catch (Exception ex) + { + Logging.SaveLog("CaptureScreen", ex); + return null; + } + } + + [SupportedOSPlatform("windows")] + private static byte[]? CaptureScreenWindows() + { + var hdcScreen = IntPtr.Zero; + var hdcMemory = IntPtr.Zero; + var hBitmap = IntPtr.Zero; + + try + { + var workArea = new RECT(); + SystemParametersInfo(SPI_GETWORKAREA, 0, ref workArea, 0); + + var left = workArea.Left; + var top = workArea.Top; + var width = workArea.Right - workArea.Left; + var height = workArea.Bottom - workArea.Top; + + if (width <= 0 || height <= 0) + { + left = 0; + top = 0; + width = GetSystemMetrics(0); + height = GetSystemMetrics(1); + } + + hdcScreen = GetDC(IntPtr.Zero); + if (hdcScreen == IntPtr.Zero) + { + return null; + } + + hdcMemory = CreateCompatibleDC(hdcScreen); + hBitmap = CreateCompatibleBitmap(hdcScreen, width, height); + + if (hBitmap == IntPtr.Zero) + { + return null; + } + + SelectObject(hdcMemory, hBitmap); + + const int SRCCOPY = 0x00CC0020; + BitBlt(hdcMemory, 0, 0, width, height, hdcScreen, left, top, SRCCOPY); + + var bmi = new BITMAPINFO + { + biSize = Marshal.SizeOf(typeof(BITMAPINFO)), + biWidth = width, + biHeight = -height, + biPlanes = 1, + biBitCount = 32, + biCompression = 0 + }; + + var imageSize = width * height * 4; + var imageData = new byte[imageSize]; + + var scanLines = GetDIBits(hdcScreen, hBitmap, 0, (uint)height, imageData, ref bmi, 0); + + if (scanLines == 0) + { + return null; + } + + using var bitmap = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul); + Marshal.Copy(imageData, 0, bitmap.GetPixels(), imageSize); + + using var image = SKImage.FromBitmap(bitmap); + using var encoded = image.Encode(SKEncodedImageFormat.Png, 100); + return encoded.ToArray(); + } + catch (Exception ex) + { + Logging.SaveLog("CaptureScreenWindows", ex); + return null; + } + finally + { + if (hBitmap != IntPtr.Zero) + { + DeleteObject(hBitmap); + } + + if (hdcMemory != IntPtr.Zero) + { + DeleteDC(hdcMemory); + } + + if (hdcScreen != IntPtr.Zero) + { + ReleaseDC(IntPtr.Zero, hdcScreen); + } + } + } + + #region Win32 API + + [LibraryImport("user32.dll")] + private static partial IntPtr GetDC(IntPtr hwnd); + + [LibraryImport("user32.dll")] + private static partial int ReleaseDC(IntPtr hwnd, IntPtr hdc); + + [LibraryImport("gdi32.dll")] + private static partial IntPtr CreateCompatibleDC(IntPtr hdc); + + [LibraryImport("gdi32.dll")] + private static partial IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight); + + [LibraryImport("gdi32.dll")] + private static partial IntPtr SelectObject(IntPtr hdc, IntPtr hObject); + + [LibraryImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, + IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop); + + [LibraryImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DeleteObject(IntPtr hObject); + + [LibraryImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DeleteDC(IntPtr hdc); + + [LibraryImport("gdi32.dll")] + private static partial int GetDIBits(IntPtr hdc, IntPtr hbmp, uint uStartScan, uint cScanLines, + byte[] lpvBits, ref BITMAPINFO lpbmi, uint uUsage); + + [LibraryImport("user32.dll")] + private static partial int GetSystemMetrics(int nIndex); + + [LibraryImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SystemParametersInfo(int uiAction, int uiParam, ref RECT pvParam, int fWinIni); + + private const int SPI_GETWORKAREA = 0x0030; + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct BITMAPINFO + { + public int biSize; + public int biWidth; + public int biHeight; + public short biPlanes; + public short biBitCount; + public int biCompression; + public int biSizeImage; + public int biXPelsPerMeter; + public int biYPelsPerMeter; + public int biClrUsed; + public int biClrImportant; + } + + #endregion Win32 API +} diff --git a/v2rayN/v2rayN.Desktop/Common/UI.cs b/v2rayN/v2rayN.Desktop/Common/UI.cs index c17d8947c0a..990e3c6817e 100644 --- a/v2rayN/v2rayN.Desktop/Common/UI.cs +++ b/v2rayN/v2rayN.Desktop/Common/UI.cs @@ -1,5 +1,5 @@ using Avalonia.Platform.Storage; -using MsBox.Avalonia; +using v2rayN.Desktop.Views; namespace v2rayN.Desktop.Common; @@ -9,8 +9,9 @@ internal class UI public static async Task ShowYesNo(Window owner, string msg) { - var box = MessageBoxManager.GetMessageBoxStandard(caption, msg, ButtonEnum.YesNo); - return await box.ShowWindowDialogAsync(owner); + var box = new MessageBoxDialog(caption, msg); + var result = await box.ShowDialog(owner); + return result == ButtonResult.Yes ? ButtonResult.Yes : ButtonResult.No; } public static async Task OpenFileDialog(Window owner, FilePickerFileType? filter) diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs index e7499b01eb8..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; @@ -20,7 +21,6 @@ global using Avalonia.Styling; global using Avalonia.Threading; global using DynamicData; -global using MsBox.Avalonia.Enums; global using ReactiveUI; global using ReactiveUI.Avalonia; global using ReactiveUI.Fody.Helpers; @@ -31,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 4dc378b48c6..35260183ecb 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -306,7 +306,7 @@ x:Name="txtSecurity5" Grid.Row="3" Grid.Column="1" - Width="200" + Width="400" Margin="{StaticResource Margin4}" HorizontalAlignment="Left" /> @@ -508,7 +508,7 @@ Grid.Row="2" ColumnDefinitions="300,Auto" IsVisible="False" - RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + Text="{x:Static resx:ResUI.TbPreSharedKey}" /> + HorizontalAlignment="Left" /> + + + + Text="{x:Static resx:ResUI.TbMtu}" /> + Watermark="1280" /> + RowDefinitions="Auto,Auto,Auto"> - - - - - - + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> - + Margin="{StaticResource Margin4}" + HorizontalAlignment="Left" /> - + + Width="400" + Margin="{StaticResource Margin4}" + HorizontalAlignment="Left" /> + + + + + Close(); cmbNetwork.SelectionChanged += CmbNetwork_SelectionChanged; + cmbHeaderTypeRaw.SelectionChanged += CmbHeaderTypeRaw_SelectionChanged; cmbStreamSecurity.SelectionChanged += CmbStreamSecurity_SelectionChanged; btnGUID.Click += btnGUID_Click; btnGUID5.Click += btnGUID_Click; @@ -24,15 +25,21 @@ public AddServerWindow(ProfileItem profileItem) cmbCoreType.ItemsSource = Global.CoreTypes.AppendEmpty(); cmbNetwork.ItemsSource = Global.Networks; + + cmbHeaderTypeRaw.ItemsSource = new List { Global.None, Global.RawHeaderHttp }; + + var kcpHeaderTypes = new List { Global.None }; + kcpHeaderTypes.AddRange(Global.KcpHeaderTypes); + cmbHeaderTypeKcp.ItemsSource = kcpHeaderTypes; + + cmbHeaderTypeXhttp.ItemsSource = Global.XhttpMode; + cmbHeaderTypeGrpc.ItemsSource = new List { Global.GrpcGunMode, Global.GrpcMultiMode }; + cmbFingerprint.ItemsSource = Global.Fingerprints; cmbFingerprint2.ItemsSource = Global.Fingerprints; - cmbAllowInsecure.ItemsSource = Global.AllowInsecure; cmbAlpn.ItemsSource = Global.Alpns; - cmbEchForceQuery.ItemsSource = Global.EchForceQuerys; - var lstStreamSecurity = new List(); - lstStreamSecurity.Add(string.Empty); - lstStreamSecurity.Add(Global.StreamSecurity); + var lstStreamSecurity = new List { string.Empty, Global.StreamSecurity }; switch (profileItem.ConfigType) { @@ -68,7 +75,7 @@ public AddServerWindow(ProfileItem profileItem) sepa2.IsVisible = false; gridTransport.IsVisible = false; cmbFingerprint.IsEnabled = false; - cmbFingerprint.SelectedValue = string.Empty; + cmbAlpn.IsEnabled = false; break; case EConfigType.TUIC: @@ -77,7 +84,6 @@ public AddServerWindow(ProfileItem profileItem) gridTransport.IsVisible = false; cmbCoreType.IsEnabled = false; cmbFingerprint.IsEnabled = false; - cmbFingerprint.SelectedValue = string.Empty; gridFinalmask.IsVisible = false; cmbCongestionControl8.ItemsSource = Global.TuicCongestionControls; @@ -108,11 +114,8 @@ public AddServerWindow(ProfileItem profileItem) cmbCoreType.IsEnabled = false; gridFinalmask.IsVisible = false; cmbFingerprint.IsEnabled = false; - cmbFingerprint.SelectedValue = string.Empty; cmbAlpn.IsEnabled = false; - cmbAlpn.SelectedValue = string.Empty; - cmbAllowInsecure.IsEnabled = false; - cmbAllowInsecure.SelectedValue = string.Empty; + togAllowInsecure.IsEnabled = false; cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls; break; @@ -134,14 +137,14 @@ public AddServerWindow(ProfileItem profileItem) this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables); break; case EConfigType.Shadowsocks: this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); break; case EConfigType.SOCKS: @@ -154,13 +157,13 @@ public AddServerWindow(ProfileItem profileItem) this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables); break; case EConfigType.Trojan: this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables); break; case EConfigType.Hysteria2: @@ -181,6 +184,7 @@ public AddServerWindow(ProfileItem profileItem) case EConfigType.WireGuard: this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgPresharedKey, v => v.txtPreSharedKey9.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(disposables); @@ -201,21 +205,41 @@ public AddServerWindow(ProfileItem profileItem) break; } this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.RequestHost, v => v.txtRequestHost.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Extra, v => v.txtExtra.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostRaw.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathRaw.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.KcpSeed, v => v.txtKcpSeed.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.KcpMtu, v => v.txtKcpMtu.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostWs.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathWs.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostHttpupgrade.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathHttpupgrade.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.XhttpMode, v => v.cmbHeaderTypeXhttp.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostXhttp.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathXhttp.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XhttpExtra, v => v.txtExtraXhttp.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.GrpcMode, v => v.cmbHeaderTypeGrpc.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.GrpcAuthority, v => v.txtRequestHostGrpc.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.GrpcServiceName, v => v.txtPathGrpc.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AllowInsecure, v => v.togAllowInsecure.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => 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.EchForceQuery, v => v.cmbEchForceQuery.SelectedValue).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); @@ -253,8 +277,12 @@ private void Window_Loaded(object? sender, RoutedEventArgs e) private void CmbNetwork_SelectionChanged(object? sender, SelectionChangedEventArgs e) { - SetHeaderType(); - SetTips(); + SetTransportGridVisibility(); + } + + private void CmbHeaderTypeRaw_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + SetRawHttpFieldsVisibility(); } private void CmbStreamSecurity_SelectionChanged(object? sender, SelectionChangedEventArgs e) @@ -283,102 +311,66 @@ private void btnGUID_Click(object? sender, RoutedEventArgs e) txtId5.Text = Utils.GetGuid(); } - private void SetHeaderType() - { - var lstHeaderType = new List(); - - var network = cmbNetwork.SelectedItem.ToString(); - if (network.IsNullOrEmpty()) - { - lstHeaderType.Add(Global.None); - cmbHeaderType.ItemsSource = lstHeaderType; - cmbHeaderType.SelectedIndex = 0; - return; - } - - if (network == nameof(ETransport.tcp)) - { - lstHeaderType.Add(Global.None); - lstHeaderType.Add(Global.TcpHeaderHttp); - } - else if (network is nameof(ETransport.kcp) or nameof(ETransport.quic)) - { - lstHeaderType.Add(Global.None); - lstHeaderType.AddRange(Global.KcpHeaderTypes); - } - else if (network is nameof(ETransport.xhttp)) - { - lstHeaderType.AddRange(Global.XhttpMode); - } - else if (network == nameof(ETransport.grpc)) - { - lstHeaderType.Add(Global.GrpcGunMode); - lstHeaderType.Add(Global.GrpcMultiMode); - } - else - { - lstHeaderType.Add(Global.None); - } - cmbHeaderType.ItemsSource = lstHeaderType; - cmbHeaderType.SelectedIndex = 0; - } - - private void SetTips() + private void SetTransportGridVisibility() { - var network = cmbNetwork.SelectedItem.ToString(); + var network = cmbNetwork.SelectedItem?.ToString(); if (network.IsNullOrEmpty()) { network = Global.DefaultNetwork; } - labHeaderType.IsVisible = true; - btnExtra.IsVisible = false; - tipRequestHost.Text = - tipPath.Text = - tipHeaderType.Text = string.Empty; + + gridTransportRaw.IsVisible = false; + gridTransportKcp.IsVisible = false; + gridTransportWs.IsVisible = false; + gridTransportHttpupgrade.IsVisible = false; + gridTransportXhttp.IsVisible = false; + gridTransportGrpc.IsVisible = false; switch (network) { - case nameof(ETransport.tcp): - tipRequestHost.Text = ResUI.TransportRequestHostTip1; - tipHeaderType.Text = ResUI.TransportHeaderTypeTip1; + case nameof(ETransport.raw): + gridTransportRaw.IsVisible = true; break; case nameof(ETransport.kcp): - tipHeaderType.Text = ResUI.TransportHeaderTypeTip2; - tipPath.Text = ResUI.TransportPathTip5; + gridTransportKcp.IsVisible = true; break; case nameof(ETransport.ws): + gridTransportWs.IsVisible = true; + break; + case nameof(ETransport.httpupgrade): - tipRequestHost.Text = ResUI.TransportRequestHostTip2; - tipPath.Text = ResUI.TransportPathTip1; + gridTransportHttpupgrade.IsVisible = true; break; case nameof(ETransport.xhttp): - tipRequestHost.Text = ResUI.TransportRequestHostTip2; - tipPath.Text = ResUI.TransportPathTip1; - tipHeaderType.Text = ResUI.TransportHeaderTypeTip5; - labHeaderType.IsVisible = false; - btnExtra.IsVisible = true; + gridTransportXhttp.IsVisible = true; break; - case nameof(ETransport.h2): - tipRequestHost.Text = ResUI.TransportRequestHostTip3; - tipPath.Text = ResUI.TransportPathTip2; + case nameof(ETransport.grpc): + gridTransportGrpc.IsVisible = true; break; - case nameof(ETransport.quic): - tipRequestHost.Text = ResUI.TransportRequestHostTip4; - tipPath.Text = ResUI.TransportPathTip3; - tipHeaderType.Text = ResUI.TransportHeaderTypeTip3; + default: + gridTransportRaw.IsVisible = true; break; + } - case nameof(ETransport.grpc): - tipRequestHost.Text = ResUI.TransportRequestHostTip5; - tipPath.Text = ResUI.TransportPathTip4; - tipHeaderType.Text = ResUI.TransportHeaderTypeTip4; - labHeaderType.IsVisible = false; - break; + SetRawHttpFieldsVisibility(); + } + + private void SetRawHttpFieldsVisibility() + { + var network = cmbNetwork.SelectedItem?.ToString(); + if (network.IsNullOrEmpty()) + { + network = Global.DefaultNetwork; } + + var rawHeaderType = cmbHeaderTypeRaw.SelectedItem?.ToString(); + var showRawHttpFields = network == nameof(ETransport.raw) + && rawHeaderType == Global.RawHeaderHttp; + gridTransportRawHttp.IsVisible = showRawHttpFields; } } 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" /> +