mirror of
https://github.com/2dust/v2rayN.git
synced 2026-10-25 22:55:01 +03:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a236fab27c | |||
| f8e1186095 | |||
| ca186e2636 | |||
| 1e8ba9ad9b | |||
| 31e32f174e | |||
| bf376a8fac | |||
| 5a9a9b7ab1 | |||
| e7a9aa66dc | |||
| 709b9a0958 | |||
| 191fe48257 | |||
| 48d58d482a | |||
| 7100b3f0a5 | |||
| 8d94997ba0 | |||
| c97dc1d894 | |||
| b198fd8be7 | |||
| e0c1ff24be | |||
| 3cebf492d1 | |||
| ef46f4e7e6 | |||
| 81c118c9b2 | |||
| f1b88ccf2a | |||
| ce380b861c | |||
| 46e9e4fead | |||
| 00903d3531 | |||
| 616eea39f9 | |||
| ff456a9389 | |||
| 0d225cd29d | |||
| 67426cff12 | |||
| a8c27027b4 | |||
| 1a681695a2 | |||
| 2126ea5a87 | |||
| be005abf9e | |||
| da7dfde706 | |||
| 7491e86d80 | |||
| af037ed087 | |||
| c9298db607 | |||
| da81101cfd | |||
| 3d4f202cd7 | |||
| 35fbb137e8 | |||
| 1869a95700 | |||
| 34359885ab | |||
| 93832656dd | |||
| b1a400b393 | |||
| 879c736924 | |||
| 8147b39723 | |||
| 5fed566a32 | |||
| f0ee792778 | |||
| cd4aa5531c | |||
| e6d08c2db1 | |||
| 4dc9c60b85 | |||
| eb9c974436 | |||
| 27b8f889fe | |||
| 0d5169e4cd | |||
| abcdd9497c | |||
| da65faa49f | |||
| b6cfce069a | |||
| 39ec9239d2 | |||
| a2a5940d91 | |||
| ce8caa89a4 | |||
| af3d579890 | |||
| b2bbc865af | |||
| 2eb325643a | |||
| 8609e2b8d1 | |||
| fcf6c1e3ae | |||
| ab907e371b | |||
| ef4dda0996 | |||
| f2e0bd4eba | |||
| bca07f801d | |||
| 54a3388c99 | |||
| 5140954b37 | |||
| c7efe39e1c | |||
| a74ca3f9d0 | |||
| ed61f5c420 | |||
| bf98c4007f | |||
| 78dcf51c3c | |||
| 1e59344074 | |||
| 1d2442d58a | |||
| 3448782925 | |||
| e6f4a57913 | |||
| 4ae5c021fd | |||
| 0a357fd1a7 | |||
| 5ba5a805ce | |||
| 807f0aba06 | |||
| f4a2086dfb | |||
| ccb0ffb3b6 | |||
| 1fa1246c0b | |||
| 14cc37d07a | |||
| c7519f8ea7 | |||
| 0c796a157b | |||
| a9824fe6ec | |||
| f18758d4bf | |||
| c1a009a409 |
@@ -100,7 +100,6 @@ csharp_style_prefer_tuple_swap = true:warning
|
|||||||
csharp_style_prefer_utf8_string_literals = true:warning
|
csharp_style_prefer_utf8_string_literals = true:warning
|
||||||
csharp_style_throw_expression = true:warning
|
csharp_style_throw_expression = true:warning
|
||||||
csharp_style_unused_value_assignment_preference = discard_variable: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_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_colon_in_constructor_initializer_experimental = true:warning
|
||||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning
|
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ body:
|
|||||||
- type: input
|
- type: input
|
||||||
id: "os-version"
|
id: "os-version"
|
||||||
attributes:
|
attributes:
|
||||||
label: "操作系统和版本"
|
label: "操作系统 / v2rayN / Core 版本"
|
||||||
description: "操作系统和版本"
|
description: "操作系统 / v2rayN / Core 版本"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,17 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
public-key:
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
uses: ./.github/workflows/pub-key.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|
||||||
update:
|
update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -70,3 +79,17 @@ jobs:
|
|||||||
\"release_tag\": \"${{ inputs.release_tag }}\"
|
\"release_tag\": \"${{ inputs.release_tag }}\"
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
|
|
||||||
|
- name: Trigger build windows x86
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
run: |
|
||||||
|
curl -X POST \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-windows-x86.yml/dispatches \
|
||||||
|
-d "{
|
||||||
|
\"ref\": \"master\",
|
||||||
|
\"inputs\": {
|
||||||
|
\"release_tag\": \"${{ inputs.release_tag }}\"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|||||||
+289
-246
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,17 +27,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: linux
|
target: linux
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
deb:
|
deb:
|
||||||
name: build and release deb x64 & arm64
|
name: build and release deb x64 & arm64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
runs-on: ubuntu-26.04
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
container: debian:13
|
container: debian:13
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Debian)
|
- name: Prepare tools (Debian)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -44,124 +42,73 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
|
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
|
||||||
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
|
|
||||||
libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
|
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with: { submodules: 'recursive', fetch-depth: '0' }
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-debian.sh
|
run: chmod 755 package-debian.sh
|
||||||
|
|
||||||
- name: Package DEB (Debian-family)
|
- name: Package DEB (Debian-family)
|
||||||
run: ./package-debian.sh "${RELEASE_TAG}" --arch all
|
run: ./package-debian.sh "${RELEASE_TAG}" --arch all
|
||||||
|
|
||||||
- name: Collect DEBs into workspace
|
- name: Collect DEBs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb"
|
||||||
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
|
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \
|
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
|
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" \
|
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
|
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
|
||||||
|
- name: Upload DEB artifact
|
||||||
- name: Upload DEBs to release
|
uses: actions/upload-artifact@v7
|
||||||
uses: svenstaro/upload-release-action@v2
|
with: { name: release-deb, path: "dist/deb/**/*.deb", if-no-files-found: error }
|
||||||
with:
|
|
||||||
file: dist/deb/**/*.deb
|
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
|
||||||
file_glob: true
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
rpm:
|
rpm:
|
||||||
name: build and release rpm x64 & arm64
|
name: build and release rpm x64 & arm64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
runs-on: ubuntu-26.04
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
container: registry.access.redhat.com/ubi10/ubi
|
container: registry.access.redhat.com/ubi10/ubi
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Red Hat)
|
- name: Prepare tools (Red Hat)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
EL_MAJOR="${VERSION_ID%%.*}"
|
EL_MAJOR="${VERSION_ID%%.*}"
|
||||||
echo "EL_MAJOR=${EL_MAJOR}"
|
echo "EL_MAJOR=${EL_MAJOR}"
|
||||||
|
|
||||||
dnf -y makecache || true
|
dnf -y makecache || true
|
||||||
command -v curl >/dev/null || dnf -y install curl ca-certificates
|
command -v curl >/dev/null || dnf -y install curl ca-certificates
|
||||||
|
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
|
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
|
||||||
|
|
||||||
install_epel_from_dir() {
|
install_epel_from_dir() {
|
||||||
local base="$1" rpm
|
local base="$1" rpm
|
||||||
echo "Try: $base"
|
echo "Try: $base"
|
||||||
|
rpm="$({ curl -fsSL "$base/Packages/" 2>/dev/null; curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'; } |
|
||||||
rpm="$(
|
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' | sort -V | tail -n1)" || true
|
||||||
{
|
[[ -n "$rpm" ]] || return 1
|
||||||
curl -fsSL "$base/Packages/" 2>/dev/null
|
|
||||||
curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'
|
|
||||||
} |
|
|
||||||
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' |
|
|
||||||
sort -V | tail -n1
|
|
||||||
)" || true
|
|
||||||
|
|
||||||
if [[ -n "$rpm" ]]; then
|
|
||||||
dnf -y install "$base/Packages/$rpm"
|
dnf -y install "$base/Packages/$rpm"
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
|
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
|
||||||
echo "Try Fedora: $FEDORA"
|
|
||||||
|
|
||||||
if curl -fsSLI "$FEDORA" >/dev/null; then
|
|
||||||
dnf -y install "$FEDORA"
|
|
||||||
else
|
|
||||||
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
|
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
|
||||||
if install_epel_from_dir "$ROCKY"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
|
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
|
||||||
if install_epel_from_dir "$ALMA"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core \
|
echo "Try Fedora: $FEDORA"
|
||||||
rsync findutils tar gzip unzip which
|
curl -fsSLI "$FEDORA" >/dev/null && dnf -y install "$FEDORA" ||
|
||||||
|
install_epel_from_dir "$ROCKY" ||
|
||||||
|
install_epel_from_dir "$ALMA" ||
|
||||||
|
{ echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"; exit 1; }
|
||||||
|
|
||||||
|
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
|
||||||
dnf repolist | grep -i epel || true
|
dnf repolist | grep -i epel || true
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with: { submodules: 'recursive', fetch-depth: '0' }
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-rhel.sh
|
run: chmod 755 package-rhel.sh
|
||||||
|
|
||||||
- name: Package RPM (RHEL-family)
|
- name: Package RPM (RHEL-family)
|
||||||
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
|
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
|
||||||
|
|
||||||
- name: Collect RPMs into workspace
|
- name: Collect RPMs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
|
||||||
@@ -170,39 +117,26 @@ jobs:
|
|||||||
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
|
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
|
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
|
||||||
|
- name: Upload RPM artifact
|
||||||
- name: Upload RPMs to release
|
uses: actions/upload-artifact@v7
|
||||||
uses: svenstaro/upload-release-action@v2
|
with: { name: release-rpm, path: "dist/rpm/**/*.rpm", if-no-files-found: error }
|
||||||
with:
|
|
||||||
file: dist/rpm/**/*.rpm
|
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
|
||||||
file_glob: true
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
rpm-riscv64:
|
rpm-riscv64:
|
||||||
name: build and release rpm riscv64
|
name: build and release rpm riscv64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04-riscv
|
runs-on: ubuntu-24.04-riscv
|
||||||
container: rockylinux/rockylinux:10
|
container: rockylinux/rockylinux:10
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Red Hat)
|
- name: Prepare tools (Red Hat)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
dnf -y makecache
|
dnf -y makecache
|
||||||
dnf -y install \
|
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which jq
|
||||||
sudo git rpm-build rpmdevtools dnf-plugins-core \
|
|
||||||
rsync findutils tar gzip unzip which jq
|
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
rm -rf ./*
|
rm -rf ./*
|
||||||
@@ -212,56 +146,27 @@ jobs:
|
|||||||
git fetch --depth=1 origin "${GITHUB_SHA}"
|
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||||
git checkout FETCH_HEAD
|
git checkout FETCH_HEAD
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-rhel-riscv.sh
|
run: chmod 755 package-rhel-riscv.sh
|
||||||
|
|
||||||
- name: Package RPM (RHEL-family)
|
- name: Package RPM (RHEL-family)
|
||||||
run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
|
run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
|
||||||
|
|
||||||
- name: Collect RPMs into workspace
|
- name: Collect RPMs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
|
||||||
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
|
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
|
||||||
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" \
|
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
|
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
|
||||||
|
- name: Upload RPM artifact
|
||||||
- name: Upload RPMs to release
|
uses: actions/upload-artifact@v7
|
||||||
shell: bash
|
with: { name: release-rpm-riscv64, path: "dist/rpm-riscv64/**/*.rpm", if-no-files-found: error }
|
||||||
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:
|
deb-riscv64:
|
||||||
name: build and release deb riscv64
|
name: build and release deb riscv64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04-riscv
|
runs-on: ubuntu-24.04-riscv
|
||||||
container: debian:13
|
container: debian:13
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Debian)
|
- name: Prepare tools (Debian)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -269,14 +174,10 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
|
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
|
||||||
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)
|
- name: Checkout repo (for scripts)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
rm -rf ./*
|
rm -rf ./*
|
||||||
@@ -286,130 +187,100 @@ jobs:
|
|||||||
git fetch --depth=1 origin "${GITHUB_SHA}"
|
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||||
git checkout FETCH_HEAD
|
git checkout FETCH_HEAD
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-debian-riscv.sh
|
run: chmod 755 package-debian-riscv.sh
|
||||||
|
|
||||||
- name: Package DEB (Debian-family)
|
- name: Package DEB (Debian-family)
|
||||||
run: ./package-debian-riscv.sh "${RELEASE_TAG}"
|
run: ./package-debian-riscv.sh "${RELEASE_TAG}"
|
||||||
|
|
||||||
- name: Collect DEBs into workspace
|
- name: Collect DEBs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
|
||||||
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
|
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" \
|
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
|
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
|
||||||
|
- name: Upload DEB artifact
|
||||||
- name: Upload DEBs to release
|
uses: actions/upload-artifact@v7
|
||||||
shell: bash
|
with: { name: release-deb-riscv64, path: "dist/deb-riscv64/**/*.deb", if-no-files-found: error }
|
||||||
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:
|
deb-loong64:
|
||||||
name: build and release deb loong64
|
name: build and release deb loong64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
runs-on: ubuntu-26.04
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||||
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.4/debian13-loong64.qcow2
|
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.4/edk2-loongarch64-code.fd
|
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.4/edk2-loongarch64-vars.fd
|
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
|
||||||
QCOW2_IMAGE: debian13-loong64.qcow2
|
QCOW2_IMAGE: debian13-loong64.qcow2
|
||||||
EFI_CODE: edk2-loongarch64-code.fd
|
EFI_CODE: edk2-loongarch64-code.fd
|
||||||
EFI_VARS: edk2-loongarch64-vars.fd
|
EFI_VARS: edk2-loongarch64-vars.fd
|
||||||
QEMU_VERSION: 10.2.2
|
QEMU_VERSION: 10.2.4
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare host tools
|
- name: Prepare host tools
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
sudo apt-get update
|
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
|
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||||
|
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||||
- name: Checkout repo
|
virtiofsd --version || virtiofsd --help | head
|
||||||
uses: actions/checkout@v6
|
- name: Host clone repo
|
||||||
with:
|
shell: bash
|
||||||
submodules: recursive
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
fetch-depth: 0
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf repo
|
||||||
|
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
|
||||||
- name: Download QEMU prebuild
|
- name: Download QEMU prebuild
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
|
||||||
wget -O qemu-linux-x64.tar.gz \
|
tar -xzf qemu-linux-x64-v3.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"
|
mkdir -p "$HOME/qemu-install"
|
||||||
|
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/"
|
||||||
rsync -a qemu-linux-x64/ "$HOME/qemu-install/"
|
|
||||||
|
|
||||||
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||||
|
|
||||||
- name: Download loong64 qcow2 and EFI firmware
|
- name: Download loong64 qcow2 and EFI firmware
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||||
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||||
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||||
|
|
||||||
qemu-img info "$QCOW2_IMAGE"
|
qemu-img info "$QCOW2_IMAGE"
|
||||||
|
|
||||||
- name: Build loong64 DEB in VM through serial console
|
- name: Build loong64 DEB in VM through serial console
|
||||||
shell: bash
|
shell: bash
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
env:
|
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
|
||||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||||
|
|
||||||
|
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
|
||||||
|
rm -f "$VIRTIOFSD_SOCKET"
|
||||||
|
|
||||||
|
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto &
|
||||||
|
|
||||||
|
VIRTIOFSD_PID=$!
|
||||||
|
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||||
|
|
||||||
|
for i in {1..100}; do
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||||
|
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||||
|
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||||
expect <<'EOF'
|
expect <<'EOF'
|
||||||
log_user 1
|
log_user 1
|
||||||
set timeout -1
|
set timeout -1
|
||||||
|
|
||||||
set release_tag $env(RELEASE_TAG)
|
set release_tag $env(RELEASE_TAG)
|
||||||
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||||
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||||
|
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||||
set workspace $env(GITHUB_WORKSPACE)
|
|
||||||
set repo $env(GITHUB_REPOSITORY)
|
|
||||||
set sha $env(GITHUB_SHA)
|
|
||||||
|
|
||||||
set qcow2 $env(QCOW2_IMAGE)
|
set qcow2 $env(QCOW2_IMAGE)
|
||||||
set efi_code $env(EFI_CODE)
|
set efi_code $env(EFI_CODE)
|
||||||
set efi_vars $env(EFI_VARS)
|
set efi_vars $env(EFI_VARS)
|
||||||
|
|
||||||
proc wait_prompt {} {
|
proc wait_prompt {} {
|
||||||
expect {
|
expect {
|
||||||
-re "__CI_PROMPT__# " {}
|
-re "__CI_PROMPT__# " {}
|
||||||
@@ -417,86 +288,258 @@ jobs:
|
|||||||
eof { exit 1 }
|
eof { exit 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proc run_cmd {cmd} {
|
proc run_cmd {cmd} {
|
||||||
send -- "$cmd\r"
|
send -- "$cmd\r"
|
||||||
wait_prompt
|
wait_prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn $qemu_bin \
|
spawn $qemu_bin \
|
||||||
-L $qemu_rom_dir \
|
-L $qemu_rom_dir \
|
||||||
-machine virt \
|
-machine virt \
|
||||||
-accel tcg,thread=multi,tb-size=2048 \
|
-accel tcg,thread=multi,tb-size=2048 \
|
||||||
-cpu la464 \
|
-cpu la464 \
|
||||||
-m 9216 \
|
-m 9216 \
|
||||||
|
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||||
|
-numa node,memdev=mem0 \
|
||||||
-smp 4 \
|
-smp 4 \
|
||||||
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||||
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||||
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||||
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||||
-netdev user,id=net0 \
|
-netdev user,id=net0 \
|
||||||
-device virtio-net-pci,netdev=net0 \
|
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||||
-virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \
|
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||||
|
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||||
-display none \
|
-display none \
|
||||||
-serial stdio \
|
-serial stdio \
|
||||||
-monitor none
|
-monitor none
|
||||||
|
|
||||||
expect -re "login:|debian login:"
|
expect -re "login:|debian login:"
|
||||||
send -- "root\r"
|
send -- "root\r"
|
||||||
|
|
||||||
expect -re "Password:|密码:|密码:"
|
expect -re "Password:|密码:|密码:"
|
||||||
send -- "password\r"
|
send -- "password\r"
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
-re "# " {}
|
-re "# " {}
|
||||||
timeout { exit 1 }
|
timeout { exit 1 }
|
||||||
eof { exit 1 }
|
eof { exit 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||||
wait_prompt
|
wait_prompt
|
||||||
|
|
||||||
run_cmd "mkdir -p /workspace"
|
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 "mount -t virtiofs workspace /workspace"
|
||||||
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
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 "dhclient \$IFACE || true"
|
||||||
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
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 "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 "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 fuse3"
|
||||||
run_cmd "rm -rf /root/v2rayN-src"
|
run_cmd "rm -rf /root/v2rayN-src"
|
||||||
run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src"
|
run_cmd "rsync -a --info=progress2 /workspace/repo/ /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 && git config --global --add safe.directory /root/v2rayN-src"
|
||||||
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
|
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"
|
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 {
|
expect {
|
||||||
-re "__BUILD_DONE__0" {
|
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
|
||||||
send -- "poweroff\r"
|
default { exit 1 }
|
||||||
}
|
|
||||||
default {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Collect DEBs
|
- name: Collect DEBs
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
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
|
||||||
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" \
|
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
|
|
||||||
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
|
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||||
|
- name: Upload DEB artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
|
||||||
|
|
||||||
- name: Upload DEBs to release
|
rpm-loong64:
|
||||||
uses: svenstaro/upload-release-action@v2
|
name: build and release rpm loong64
|
||||||
with:
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
file: dist/deb-loong64/**/*.deb
|
runs-on: ubuntu-26.04
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
env:
|
||||||
file_glob: true
|
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||||
prerelease: true
|
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
|
||||||
|
EFI_CODE_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-code.fd
|
||||||
|
EFI_VARS_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-vars.fd
|
||||||
|
QCOW2_IMAGE: fedora43-loong64.qcow2
|
||||||
|
EFI_CODE: edk2-loongarch64-code.fd
|
||||||
|
EFI_VARS: edk2-loongarch64-vars.fd
|
||||||
|
QEMU_VERSION: 10.2.4
|
||||||
|
steps:
|
||||||
|
- name: Prepare host tools
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||||
|
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||||
|
virtiofsd --version || virtiofsd --help | head
|
||||||
|
- name: Host clone repo
|
||||||
|
shell: bash
|
||||||
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf repo
|
||||||
|
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
|
||||||
|
- name: Download QEMU prebuild
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
|
||||||
|
tar -xzf qemu-linux-x64-v3.tar.gz
|
||||||
|
mkdir -p "$HOME/qemu-install"
|
||||||
|
rsync -a qemu-linux-x64-v3/ "$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 RPM 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/rpm-loong64"
|
||||||
|
|
||||||
|
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
|
||||||
|
rm -f "$VIRTIOFSD_SOCKET"
|
||||||
|
|
||||||
|
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto &
|
||||||
|
|
||||||
|
VIRTIOFSD_PID=$!
|
||||||
|
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||||
|
|
||||||
|
for i in {1..100}; do
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||||
|
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||||
|
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||||
|
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 virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||||
|
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 \
|
||||||
|
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||||
|
-numa node,memdev=mem0 \
|
||||||
|
-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-non-transitional,netdev=net0 \
|
||||||
|
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||||
|
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||||
|
-display none \
|
||||||
|
-serial stdio \
|
||||||
|
-monitor none
|
||||||
|
expect -re "login:|fedora 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 virtiofs 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 "dnf -y makecache || dnf -y makecache || dnf -y makecache"
|
||||||
|
run_cmd "dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 --nogpgcheck"
|
||||||
|
run_cmd "rm -rf /root/v2rayN-src"
|
||||||
|
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
|
||||||
|
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||||
|
run_cmd "cd /root/v2rayN-src && chmod 755 package-rhel-loong.sh"
|
||||||
|
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-rpm-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-rhel-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/rpm-loong64\nfind /root/rpmbuild/RPMS -name '*.rpm' -exec cp -av {} /workspace/dist/rpm-loong64/ \\; 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-rpm-build.sh\r"
|
||||||
|
expect {
|
||||||
|
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
|
||||||
|
default { exit 1 }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
- name: Collect RPMs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||||
|
find "$GITHUB_WORKSPACE/dist/rpm-loong64" -name "*.loongarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-loong64/v2rayN-linux-rhel-loong64.rpm" \; || true
|
||||||
|
echo "==== Dist tree ===="
|
||||||
|
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||||
|
- name: Upload RPM artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with: { name: release-rpm-loong64, path: "dist/rpm-loong64/**/*.rpm", if-no-files-found: error }
|
||||||
|
|
||||||
|
release-deb:
|
||||||
|
name: sign and upload deb x64 arm64
|
||||||
|
needs: deb
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb, file_pattern: "*.deb" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-rpm:
|
||||||
|
name: sign and upload rpm x64 arm64
|
||||||
|
needs: rpm
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm, file_pattern: "*.rpm" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-rpm-riscv64:
|
||||||
|
name: sign and upload rpm riscv64
|
||||||
|
needs: rpm-riscv64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-riscv64, file_pattern: "*.rpm" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-deb-riscv64:
|
||||||
|
name: sign and upload deb riscv64
|
||||||
|
needs: deb-riscv64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-riscv64, file_pattern: "*.deb" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-deb-loong64:
|
||||||
|
name: sign and upload deb loong64
|
||||||
|
needs: deb-loong64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-loong64, file_pattern: "*.deb" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-rpm-loong64:
|
||||||
|
name: sign and upload rpm loong64
|
||||||
|
needs: rpm-loong64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-loong64, file_pattern: "*.rpm" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,6 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: macos
|
target: macos
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
name: package and release macOS dmg
|
name: package and release macOS dmg
|
||||||
@@ -45,7 +47,7 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
@@ -62,13 +64,24 @@ jobs:
|
|||||||
- name: Package dmg
|
- name: Package dmg
|
||||||
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
|
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
|
||||||
|
|
||||||
- name: Sleep for race condition between matrix jobs
|
- name: Upload dmg artifact
|
||||||
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
|
uses: actions/upload-artifact@v7
|
||||||
|
|
||||||
- name: Upload dmg to release
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ${{ github.workspace }}/v2rayN*.dmg
|
name: release-dmg-${{ matrix.arch }}
|
||||||
tag: ${{ inputs.release_tag }}
|
path: v2rayN*.dmg
|
||||||
file_glob: true
|
if-no-files-found: error
|
||||||
prerelease: true
|
|
||||||
|
release-dmg:
|
||||||
|
name: sign and upload macOS dmg
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
needs: dmg
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: release-dmg-${{ matrix.arch }}
|
||||||
|
file_pattern: '*.dmg'
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,3 +27,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: windows-desktop
|
target: windows-desktop
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
name: release Windows x86
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package-x86:
|
||||||
|
name: build and release windows x86 WPF & Avalonia
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- flavor: wpf
|
||||||
|
project: ./v2rayN/v2rayN/v2rayN.csproj
|
||||||
|
zip_name: v2rayN-windows-86.zip
|
||||||
|
- flavor: desktop
|
||||||
|
project: ./v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
|
||||||
|
zip_name: v2rayN-windows-86-desktop.zip
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Purge SDKs
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
echo "Before:"
|
||||||
|
dotnet --list-sdks
|
||||||
|
|
||||||
|
dotnet --list-sdks | % {
|
||||||
|
$_ -match '^\S+\s+\[(.+)\]$' > $null
|
||||||
|
$d = Join-Path $Matches[1] $_.Split()[0]
|
||||||
|
Write-Host "Removing $d"
|
||||||
|
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "After:"
|
||||||
|
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
|
||||||
|
|
||||||
|
- name: Setup .NET 10.0.1xx
|
||||||
|
uses: actions/setup-dotnet@v5.4.0
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.1xx
|
||||||
|
|
||||||
|
- name: Publish ${{ matrix.flavor }} win-x86
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
dotnet restore "${{ matrix.project }}" -r win-x86
|
||||||
|
|
||||||
|
dotnet publish "${{ matrix.project }}" `
|
||||||
|
-c Release `
|
||||||
|
-r win-x86 `
|
||||||
|
-p:SelfContained=true `
|
||||||
|
-p:EnableWindowsTargeting=true `
|
||||||
|
-o "publish/${{ matrix.flavor }}"
|
||||||
|
|
||||||
|
- name: Publish AmazTool win-x86
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
dotnet publish "./v2rayN/AmazTool/AmazTool.csproj" `
|
||||||
|
-c Release `
|
||||||
|
-r win-x86 `
|
||||||
|
-p:SelfContained=true `
|
||||||
|
-p:PublishTrimmed=true `
|
||||||
|
-p:EnableWindowsTargeting=true `
|
||||||
|
-o "publish/${{ matrix.flavor }}"
|
||||||
|
|
||||||
|
- name: Detect bundled core versions
|
||||||
|
id: detect-core
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path "tmp/probe" | Out-Null
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/xray/xray.exe" -OutFile "tmp/probe/xray.exe"
|
||||||
|
$xrayOutput = (& "tmp/probe/xray.exe" version) -join "`n"
|
||||||
|
Write-Host $xrayOutput
|
||||||
|
|
||||||
|
$xrayMatch = [regex]::Match($xrayOutput, 'Xray\s+([0-9.]+)')
|
||||||
|
if ($xrayMatch.Success) {
|
||||||
|
$xrayVer = $xrayMatch.Groups[1].Value
|
||||||
|
} else {
|
||||||
|
throw "Failed to detect Xray version"
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/sing_box/sing-box.exe" -OutFile "tmp/probe/sing-box.exe"
|
||||||
|
$singOutput = (& "tmp/probe/sing-box.exe" version) -join "`n"
|
||||||
|
Write-Host $singOutput
|
||||||
|
|
||||||
|
$singMatch = [regex]::Match($singOutput, 'sing-box version\s+([0-9.]+)')
|
||||||
|
if ($singMatch.Success) {
|
||||||
|
$singVer = $singMatch.Groups[1].Value
|
||||||
|
} else {
|
||||||
|
throw "Failed to detect sing-box version"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Bundled Xray version: $xrayVer"
|
||||||
|
Write-Host "Bundled sing-box version: $singVer"
|
||||||
|
|
||||||
|
"xray_version=$xrayVer" >> $env:GITHUB_OUTPUT
|
||||||
|
"sing_version=$singVer" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Download x86 cores and rule assets
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$headers = @{
|
||||||
|
Authorization = "Bearer $env:GH_TOKEN"
|
||||||
|
Accept = "application/vnd.github+json"
|
||||||
|
"X-GitHub-Api-Version" = "2022-11-28"
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = "publish/${{ matrix.flavor }}"
|
||||||
|
$bin = Join-Path $root "bin"
|
||||||
|
$xrayDir = Join-Path $bin "xray"
|
||||||
|
$singDir = Join-Path $bin "sing_box"
|
||||||
|
$srssDir = Join-Path $bin "srss"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $xrayDir, $singDir, $srssDir | Out-Null
|
||||||
|
|
||||||
|
$xrayVer = "${{ steps.detect-core.outputs.xray_version }}"
|
||||||
|
$singVer = "${{ steps.detect-core.outputs.sing_version }}"
|
||||||
|
|
||||||
|
$xrayTag = "v$xrayVer"
|
||||||
|
$singTag = "v$singVer"
|
||||||
|
|
||||||
|
$xrayUrl = "https://github.com/autorepobot/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||||
|
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
||||||
|
|
||||||
|
Write-Host "Bundled Xray version: $xrayVer"
|
||||||
|
Write-Host "Bundled sing-box version: $singVer"
|
||||||
|
Write-Host "Xray: $xrayUrl"
|
||||||
|
Write-Host "sing-box: $singUrl"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri $xrayUrl -OutFile "tmp/xray.zip"
|
||||||
|
Expand-Archive -Force "tmp/xray.zip" "tmp/xray"
|
||||||
|
|
||||||
|
Get-ChildItem "tmp/xray" -Recurse -Include "*.exe","*.dll" | ForEach-Object {
|
||||||
|
Copy-Item $_.FullName (Join-Path $xrayDir $_.Name) -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri $singUrl -OutFile "tmp/sing-box.zip"
|
||||||
|
Expand-Archive -Force "tmp/sing-box.zip" "tmp/sing-box"
|
||||||
|
|
||||||
|
$singExe = Get-ChildItem "tmp/sing-box" -Recurse -Filter "sing-box.exe" | Select-Object -First 1
|
||||||
|
if (-not $singExe) {
|
||||||
|
throw "sing-box.exe not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item $singExe.FullName "$singDir/sing-box.exe" -Force
|
||||||
|
|
||||||
|
$cronet = Get-ChildItem "tmp/sing-box" -Recurse -Filter "libcronet*.dll" | Select-Object -First 1
|
||||||
|
if ($cronet) {
|
||||||
|
Copy-Item $cronet.FullName "$singDir/libcronet.dll" -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "$bin/geosite.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "$bin/geoip.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat" -OutFile "$bin/geoip-only-cn-private.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" -OutFile "$bin/Country.mmdb"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" -OutFile "$bin/geoip.metadb"
|
||||||
|
|
||||||
|
$geoipRules = @(
|
||||||
|
"geoip-private.srs",
|
||||||
|
"geoip-cn.srs",
|
||||||
|
"geoip-facebook.srs",
|
||||||
|
"geoip-fastly.srs",
|
||||||
|
"geoip-google.srs",
|
||||||
|
"geoip-netflix.srs",
|
||||||
|
"geoip-telegram.srs",
|
||||||
|
"geoip-twitter.srs"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($f in $geoipRules) {
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f" -OutFile "$srssDir/$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
$geositeRules = @(
|
||||||
|
"geosite-cn.srs",
|
||||||
|
"geosite-gfw.srs",
|
||||||
|
"geosite-google.srs",
|
||||||
|
"geosite-greatfire.srs",
|
||||||
|
"geosite-geolocation-cn.srs",
|
||||||
|
"geosite-category-ads-all.srs",
|
||||||
|
"geosite-private.srs"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($f in $geositeRules) {
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f" -OutFile "$srssDir/$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Package zip
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Compress-Archive `
|
||||||
|
-Path "publish/${{ matrix.flavor }}/*" `
|
||||||
|
-DestinationPath "${{ matrix.zip_name }}" `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
- name: Upload zip artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: release-windows-x86-${{ matrix.flavor }}
|
||||||
|
path: ${{ matrix.zip_name }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release-x86:
|
||||||
|
name: sign and upload windows x86
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
needs: package-x86
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ wpf, desktop ]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: release-windows-x86-${{ matrix.flavor }}
|
||||||
|
file_pattern: '*.zip'
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -27,3 +28,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: windows
|
target: windows
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ jobs:
|
|||||||
case(
|
case(
|
||||||
inputs.target == 'macos', 'macos-latest',
|
inputs.target == 'macos', 'macos-latest',
|
||||||
inputs.target == 'linux', 'ubuntu-24.04',
|
inputs.target == 'linux', 'ubuntu-24.04',
|
||||||
|
inputs.target == 'windows', 'windows-latest',
|
||||||
'ubuntu-latest'
|
'ubuntu-latest'
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -46,21 +47,39 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Purge SDKs
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
echo "Before:"
|
||||||
|
dotnet --list-sdks
|
||||||
|
|
||||||
|
dotnet --list-sdks | % {
|
||||||
|
$_ -match '^\S+\s+\[(.+)\]$' > $null
|
||||||
|
$d = Join-Path $Matches[1] $_.Split()[0]
|
||||||
|
Write-Host "Removing $d"
|
||||||
|
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "After:"
|
||||||
|
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5.2.0
|
uses: actions/setup-dotnet@v5.4.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.1xx'
|
dotnet-version: '10.0.1xx'
|
||||||
|
|
||||||
- name: Build v2rayN
|
- name: Build v2rayN
|
||||||
|
shell: bash
|
||||||
working-directory: ./v2rayN
|
working-directory: ./v2rayN
|
||||||
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
|
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
|
||||||
|
|
||||||
- name: Build AmazTool
|
- name: Build AmazTool
|
||||||
|
shell: bash
|
||||||
working-directory: ./v2rayN
|
working-directory: ./v2rayN
|
||||||
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
|
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ on:
|
|||||||
target: # windows linux macos windows-desktop
|
target: # windows linux macos windows-desktop
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
package:
|
package:
|
||||||
@@ -37,7 +41,7 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
@@ -55,13 +59,23 @@ jobs:
|
|||||||
if: inputs.target == 'windows-desktop'
|
if: inputs.target == 'windows-desktop'
|
||||||
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
|
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
|
||||||
|
|
||||||
- name: Sleep for race condition between matrix jobs
|
- name: Upload zip artifact
|
||||||
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
|
uses: actions/upload-artifact@v7
|
||||||
|
|
||||||
- name: Upload zip archive to release
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ${{ github.workspace }}/v2rayN*.zip
|
name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||||
tag: ${{ inputs.release_tag }}
|
path: v2rayN*.zip
|
||||||
file_glob: true
|
if-no-files-found: error
|
||||||
prerelease: true
|
|
||||||
|
release:
|
||||||
|
name: sign and upload zip x64 arm64
|
||||||
|
needs: package
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||||
|
file_pattern: '*.zip'
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: publish GPG public key
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
steps:
|
||||||
|
- name: Import GPG private key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 700 -d ~/.gnupg
|
||||||
|
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
|
||||||
|
- name: Export GPG public key
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
fingerprint="$(gpg --batch --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10; exit }')"
|
||||||
|
[[ -n "$fingerprint" ]] || { echo "No GPG secret key found."; exit 1; }
|
||||||
|
gpg --batch --armor --export "$fingerprint" > v2rayN-public-key.asc
|
||||||
|
|
||||||
|
- name: Upload GPG public key to release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
file: v2rayN-public-key.asc
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
@@ -14,13 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5.2.0
|
uses: actions/setup-dotnet@v5.4.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: sign and upload release files
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
artifact_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
file_pattern:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: '**/*'
|
||||||
|
content_type:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: application/octet-stream
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload:
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
steps:
|
||||||
|
- name: Download release artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact_name }}
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Import GPG private key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 700 -d ~/.gnupg
|
||||||
|
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
|
||||||
|
- name: Sign release files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd dist
|
||||||
|
shopt -s globstar nullglob
|
||||||
|
files=( ${{ inputs.file_pattern }} )
|
||||||
|
(( ${#files[@]} )) || { echo "No files matched: ${{ inputs.file_pattern }}"; exit 1; }
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
gpg --batch --yes --armor --detach-sign --output "$f.sig" "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Upload release files
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
file: dist/${{ inputs.file_pattern }}
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
file_glob: true
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Upload release signatures
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
file: dist/${{ inputs.file_pattern }}.sig
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
file_glob: true
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
@@ -84,6 +84,9 @@ StyleCopReport.xml
|
|||||||
*.rsp
|
*.rsp
|
||||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||||
!Directory.Build.rsp
|
!Directory.Build.rsp
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
build/
|
||||||
*.sbr
|
*.sbr
|
||||||
*.tlb
|
*.tlb
|
||||||
*.tli
|
*.tli
|
||||||
|
|||||||
@@ -1,18 +1,78 @@
|
|||||||
# v2rayN
|
# v2rayN
|
||||||
|
|
||||||
A GUI client for Windows, Linux and macOS, support [Xray](https://github.com/XTLS/Xray-core)
|
### A GUI client for Windows, Linux and macOS. Support [Xray](https://github.com/XTLS/Xray-core) and [sing-box](https://github.com/SagerNet/sing-box) and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
|
||||||
and [sing-box](https://github.com/SagerNet/sing-box)
|
|
||||||
and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
|
|
||||||
|
|
||||||
[](https://github.com/2dust/v2rayN/commits/master)
|
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayn)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayn)
|
||||||
[](https://github.com/2dust/v2rayN/releases)
|
[](https://github.com/2dust/v2rayN/releases)
|
||||||
[](https://t.me/v2rayn)
|
[](https://github.com/2dust/v2rayN/releases)
|
||||||
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
## How to use
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
|
||||||
Read the [Wiki](https://github.com/2dust/v2rayN/wiki) for details.
|
|
||||||
|
|
||||||
## Telegram Channel
|
---
|
||||||
|
|
||||||
[github_2dust](https://t.me/github_2dust)
|
## Download / 下载
|
||||||
|
|
||||||
|
Download the latest release here:
|
||||||
|
|
||||||
|
在这里下载最新版本:
|
||||||
|
|
||||||
|
[https://github.com/2dust/v2rayN/releases](https://github.com/2dust/v2rayN/releases)
|
||||||
|
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> v2rayN is the desktop version. For the mobile version, please visit the v2rayNG \
|
||||||
|
> v2rayN 是电脑版,手机版请访问 v2rayNG
|
||||||
|
>
|
||||||
|
> https://github.com/2dust/v2rayNG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation / 使用文档
|
||||||
|
|
||||||
|
Read the Wiki for usage guides and configuration details.
|
||||||
|
|
||||||
|
请阅读 Wiki 获取使用说明和配置教程。
|
||||||
|
|
||||||
|
[https://github.com/2dust/v2rayN/wiki](https://github.com/2dust/v2rayN/wiki)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Platforms / 支持平台
|
||||||
|
|
||||||
|
| Platform / 平台 | x64 | x86 | arm64 | riscv64 | loong64 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| Windows | ✅ | ✅ | ✅ | - | - |
|
||||||
|
| Linux | ✅ | - | ✅ | ✅ | ✅ |
|
||||||
|
| macOS | ✅ | - | ✅ | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPG Verification / GPG 签名校验
|
||||||
|
|
||||||
|
Release files are signed with GPG to verify authenticity and integrity, helping prevent mirror, ISP, or CDN hijacking.
|
||||||
|
|
||||||
|
发布文件已使用 GPG 签名,可用于校验文件真实性与完整性,预防镜像站、运营商或 CDN 劫持。
|
||||||
|
|
||||||
|
### Fingerprint / 公钥指纹
|
||||||
|
|
||||||
|
```text
|
||||||
|
7694 5E9F 3E9A 168F 8070 F195 805D 661C
|
||||||
|
134D FAF6 8903 C199 463C 31E5 AE90 3AE0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Community / 社区
|
||||||
|
|
||||||
|
Telegram Group / Telegram 群组:
|
||||||
|
|
||||||
|
[https://t.me/v2rayN](https://t.me/v2rayN)
|
||||||
|
|
||||||
|
Telegram Channel / Telegram 频道:
|
||||||
|
|
||||||
|
[https://t.me/github_2dust](https://t.me/github_2dust)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ PKGROOT="v2rayN-publish"
|
|||||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
OUTPUT_DIR="${HOME}/debbuild"
|
OUTPUT_DIR="${HOME}/debbuild"
|
||||||
DOTNET_TFM="net10.0"
|
DOTNET_TFM="net10.0"
|
||||||
DOTNET_LOONGARCH_VERSION="10.0.108"
|
DOTNET_LOONGARCH_VERSION="10.0.109"
|
||||||
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64"
|
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64"
|
||||||
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
|
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
|
||||||
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
|
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
|
||||||
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
|
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
|
|||||||
PKGROOT="v2rayN-publish"
|
PKGROOT="v2rayN-publish"
|
||||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
OUTPUT_DIR="${HOME}/debbuild"
|
OUTPUT_DIR="${HOME}/debbuild"
|
||||||
DOTNET_RISCV_VERSION="10.0.108"
|
DOTNET_RISCV_VERSION="10.0.109"
|
||||||
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
||||||
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
||||||
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
||||||
|
|||||||
+11
-1
@@ -22,7 +22,17 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>en</string>
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>zh-Hans</string>
|
||||||
|
<string>zh-Hant</string>
|
||||||
|
<string>en</string>
|
||||||
|
<string>fa</string>
|
||||||
|
<string>fr</string>
|
||||||
|
<string>ru</string>
|
||||||
|
<string>hu</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>v2rayN</string>
|
<string>v2rayN</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
|||||||
@@ -0,0 +1,709 @@
|
|||||||
|
#!/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_LOONGARCH_VERSION="10.0.109"
|
||||||
|
DOTNET_LOONGARCH_TAG="v10.0.109-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_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
|
||||||
|
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=""
|
||||||
|
|
||||||
|
if command -v dnf >/dev/null 2>&1; then
|
||||||
|
sudo dnf -y --nogpgcheck 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_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
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
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-loongarch 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/tty; then
|
||||||
|
case "${sel:-}" in
|
||||||
|
2) ch="prerelease" ;;
|
||||||
|
3) ch="keep" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$ch"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_tag_latest() {
|
||||||
|
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
|
||||||
|
| jq -re '.tag_name' \
|
||||||
|
| sed 's/^v//'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_tag_prerelease() {
|
||||||
|
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20" \
|
||||||
|
| jq -re 'first(.[] | select(.prerelease == true) | .tag_name)' \
|
||||||
|
| 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"
|
||||||
|
local ref=""
|
||||||
|
|
||||||
|
if git rev-parse --git-dir >/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%s\n' "linux-loongarch64" "loongarch64" "loongarch64" ;;
|
||||||
|
*) 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_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
|
||||||
|
License: GPL-3.0-only
|
||||||
|
URL: https://github.com/2dust/v2rayN
|
||||||
|
BugURL: https://github.com/2dust/v2rayN/issues
|
||||||
|
ExclusiveArch: loongarch64
|
||||||
|
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' loongarch64
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "$@"
|
||||||
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
|
|||||||
PKGROOT="v2rayN-publish"
|
PKGROOT="v2rayN-publish"
|
||||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
RPM_TOPDIR="${HOME}/rpmbuild"
|
RPM_TOPDIR="${HOME}/rpmbuild"
|
||||||
DOTNET_RISCV_VERSION="10.0.108"
|
DOTNET_RISCV_VERSION="10.0.109"
|
||||||
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
||||||
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
||||||
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
||||||
@@ -501,7 +501,7 @@ write_spec_file() {
|
|||||||
Name: v2rayN
|
Name: v2rayN
|
||||||
Version: __VERSION__
|
Version: __VERSION__
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: v2rayN (Avalonia) GUI client for Linux (riscv64)
|
Summary: v2rayN (Avalonia) GUI client for Linux
|
||||||
License: GPL-3.0-only
|
License: GPL-3.0-only
|
||||||
URL: https://github.com/2dust/v2rayN
|
URL: https://github.com/2dust/v2rayN
|
||||||
BugURL: https://github.com/2dust/v2rayN/issues
|
BugURL: https://github.com/2dust/v2rayN/issues
|
||||||
|
|||||||
+1
-1
@@ -490,7 +490,7 @@ write_spec_file() {
|
|||||||
Name: v2rayN
|
Name: v2rayN
|
||||||
Version: __VERSION__
|
Version: __VERSION__
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
|
Summary: v2rayN (Avalonia) GUI client for Linux
|
||||||
License: GPL-3.0-only
|
License: GPL-3.0-only
|
||||||
URL: https://github.com/2dust/v2rayN
|
URL: https://github.com/2dust/v2rayN
|
||||||
BugURL: https://github.com/2dust/v2rayN/issues
|
BugURL: https://github.com/2dust/v2rayN/issues
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.22.2</Version>
|
<Version>7.23.2</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -7,33 +7,33 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
|
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
|
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" />
|
||||||
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
|
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
|
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.1" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" />
|
||||||
<PackageVersion Include="Downloader" Version="5.5.0" />
|
<PackageVersion Include="CliWrap" Version="3.10.2" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
<PackageVersion Include="Downloader" Version="5.9.0" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||||
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
|
<PackageVersion Include="ReactiveUI" Version="23.2.28" />
|
||||||
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
|
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
|
||||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
|
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
|
||||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
|
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
|
||||||
<PackageVersion Include="NLog" Version="6.1.3" />
|
<PackageVersion Include="NLog" Version="6.1.3" />
|
||||||
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
|
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
|
||||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.4" />
|
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
|
||||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
|
<PackageVersion Include="YamlDotNet" Version="18.0.0" />
|
||||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ internal static class CoreConfigTestFactory
|
|||||||
{
|
{
|
||||||
return new Config
|
return new Config
|
||||||
{
|
{
|
||||||
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false },
|
CoreBasicItem = new CoreBasicItem { Loglevel = "warning" },
|
||||||
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
|
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
|
||||||
KcpItem = new KcpItem(),
|
KcpItem = new KcpItem(),
|
||||||
GrpcItem = new GrpcItem(),
|
GrpcItem = new GrpcItem(),
|
||||||
@@ -187,7 +187,7 @@ internal static class CoreConfigTestFactory
|
|||||||
SimpleDnsItem = config.SimpleDNSItem,
|
SimpleDnsItem = config.SimpleDNSItem,
|
||||||
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
|
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
|
||||||
FullConfigTemplate = null,
|
FullConfigTemplate = null,
|
||||||
IsTunEnabled = false,
|
IsTunEnabled = config.TunModeItem.EnableTun,
|
||||||
ProtectDomainList = [],
|
ProtectDomainList = [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -206,4 +206,12 @@ internal static class CoreConfigTestFactory
|
|||||||
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
|
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
|
||||||
return config;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using AwesomeAssertions;
|
using AwesomeAssertions;
|
||||||
using ServiceLib.Common;
|
using ServiceLib.Common;
|
||||||
using ServiceLib.Enums;
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Handler.Fmt;
|
||||||
using ServiceLib.Manager;
|
using ServiceLib.Manager;
|
||||||
using ServiceLib.Models;
|
using ServiceLib.Models;
|
||||||
|
using ServiceLib.Models.Dto;
|
||||||
using ServiceLib.Services.CoreConfig;
|
using ServiceLib.Services.CoreConfig;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -554,7 +556,40 @@ public class CoreConfigSingboxServiceTests
|
|||||||
|
|
||||||
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 == "remote" && s.type == "udp" && s.server == "8.8.8.8");
|
||||||
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
|
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 == nameof(ERuleMode.Global));
|
||||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString());
|
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
|
||||||
|
{
|
||||||
|
var shareLink =
|
||||||
|
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
|
||||||
|
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
|
||||||
|
node.Should().NotBeNull();
|
||||||
|
node!.CoreType = ECoreType.sing_box;
|
||||||
|
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.CoreTypeItem =
|
||||||
|
[
|
||||||
|
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
|
||||||
|
];
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
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<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||||
|
|
||||||
|
proxy.type.Should().Be("hysteria2");
|
||||||
|
proxy.realm.Should().NotBeNull();
|
||||||
|
proxy.realm!.server_url.Should().StartWith("https://");
|
||||||
|
proxy.realm.server_url.Should().Contain("realm.hy2.io");
|
||||||
|
proxy.realm.token.Should().Be("public");
|
||||||
|
proxy.realm.realm_id.Should().Be("my-realm-id");
|
||||||
|
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
|
||||||
|
proxy.server.Should().BeNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -536,4 +536,27 @@ public class CoreConfigV2rayServiceTests
|
|||||||
directOutbound.Should().NotBeNull();
|
directOutbound.Should().NotBeNull();
|
||||||
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4");
|
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<V2rayConfig>(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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Handler.Fmt;
|
||||||
|
using ServiceLib.Models.Dto;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.Fmt;
|
||||||
|
|
||||||
|
public class HyRealmTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_ShouldParseValidRealm()
|
||||||
|
{
|
||||||
|
var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478";
|
||||||
|
var result = HyRealm.TryParse(str, out var realm);
|
||||||
|
result.Should().BeTrue();
|
||||||
|
realm.Should().NotBeNull();
|
||||||
|
|
||||||
|
realm.IsHttp.Should().BeFalse();
|
||||||
|
realm.Token.Should().Be("public");
|
||||||
|
realm.RendezvousHost.Should().Be("realm.hy2.io");
|
||||||
|
realm.RendezvousPort.Should().Be(443);
|
||||||
|
realm.RealmName.Should().Be("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||||
|
realm.StunList.Should().HaveCount(2);
|
||||||
|
realm.StunList.Should().Contain("example.stun:3478");
|
||||||
|
realm.StunList.Should().Contain("example2.stun:3478");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToUri_ShouldGenerateValidUri()
|
||||||
|
{
|
||||||
|
var realm = new HyRealm(
|
||||||
|
IsHttp: false,
|
||||||
|
Token: "public",
|
||||||
|
RendezvousHost: "realm.hy2.io",
|
||||||
|
RendezvousPort: 443,
|
||||||
|
RealmName: "57f9be7c-2810-4f5b-8cb9-260bc84d6c90",
|
||||||
|
StunList: ["example.stun:3478", "example2.stun:3478"]
|
||||||
|
);
|
||||||
|
var uri = realm.ToUri();
|
||||||
|
uri.Should().Contain("realm://public@realm.hy2.io");
|
||||||
|
uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||||
|
uri.Should().Contain("stun=example.stun:3478");
|
||||||
|
uri.Should().Contain("stun=example2.stun:3478");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
|
||||||
|
{
|
||||||
|
var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark";
|
||||||
|
var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg);
|
||||||
|
resolved.Should().NotBeNull();
|
||||||
|
resolved.Password.Should().Be("your_password");
|
||||||
|
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
|
||||||
|
result.Should().BeTrue();
|
||||||
|
realm.Should().NotBeNull();
|
||||||
|
realm.Token.Should().Be("mytoken");
|
||||||
|
|
||||||
|
// To uri
|
||||||
|
var uri = Hysteria2Fmt.ToUri(resolved);
|
||||||
|
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
|
||||||
|
uri.Should().EndWith("#remark");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToServerUrl_ShouldIncludeSchemeForSingbox()
|
||||||
|
{
|
||||||
|
var realm = new HyRealm(
|
||||||
|
IsHttp: false,
|
||||||
|
Token: "public",
|
||||||
|
RendezvousHost: "realm.hy2.io",
|
||||||
|
RendezvousPort: 443,
|
||||||
|
RealmName: "my-realm-id",
|
||||||
|
StunList: ["turn.cloudflare.com:3478"]
|
||||||
|
);
|
||||||
|
|
||||||
|
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
|
||||||
|
{
|
||||||
|
var str = "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
|
||||||
|
var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
|
||||||
|
resolved.Should().NotBeNull();
|
||||||
|
|
||||||
|
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
|
||||||
|
realm!.ToServerUrl().Should().StartWith("https://");
|
||||||
|
realm.ToServerUrl().Should().Contain("realm.hy2.io");
|
||||||
|
realm.StunList.Should().Contain("turn.cloudflare.com:3478");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
namespace ServiceLib.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for country code utilities
|
||||||
|
/// </summary>
|
||||||
|
public static class CountryExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Country code to emoji flag mapping for common countries
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<string, string> 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
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts country code to flag emoji using predefined mapping
|
||||||
|
/// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳"
|
||||||
|
/// </summary>
|
||||||
|
public static string? CountryToEmoji(this string? countryCode)
|
||||||
|
{
|
||||||
|
if (countryCode.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CountryEmojiMap.GetValueOrDefault(countryCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ public static class Extension
|
|||||||
|
|
||||||
public static string TrimEx(this string? value)
|
public static string TrimEx(this string? value)
|
||||||
{
|
{
|
||||||
return value == null ? string.Empty : value.Trim();
|
return value?.Trim() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RemovePrefix(this string value, char prefix)
|
public static string RemovePrefix(this string value, char prefix)
|
||||||
@@ -118,4 +118,20 @@ public static class Extension
|
|||||||
destination.Add(item);
|
destination.Add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replace all cross-platform newline characters with the specified string
|
||||||
|
/// </summary>
|
||||||
|
public static string ReplaceLineBreaks(this string input, string replacement)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// You must replace \r\n first, and then replace the single characters \r and \n.
|
||||||
|
return input.Replace("\r\n", replacement)
|
||||||
|
.Replace("\r", replacement)
|
||||||
|
.Replace("\n", replacement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ public static class FileUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine)
|
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine, string? contains = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -214,6 +214,10 @@ public static class FileUtils
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (contains.IsNotEmpty() && !file.Name.Contains(contains))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
file.Delete();
|
file.Delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class Utils
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
str = str.Replace(Environment.NewLine, string.Empty);
|
str = str.ReplaceLineBreaks(string.Empty);
|
||||||
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -114,9 +114,7 @@ public class Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
plainText = plainText.Trim()
|
plainText = plainText.Trim()
|
||||||
.Replace(Environment.NewLine, "")
|
.ReplaceLineBreaks("")
|
||||||
.Replace("\n", "")
|
|
||||||
.Replace("\r", "")
|
|
||||||
.Replace('_', '/')
|
.Replace('_', '/')
|
||||||
.Replace('-', '+')
|
.Replace('-', '+')
|
||||||
.Replace(" ", "");
|
.Replace(" ", "");
|
||||||
@@ -321,7 +319,32 @@ public class Utils
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.Replace(",", ",").Replace(Environment.NewLine, ",");
|
return text.Replace(",", ",")
|
||||||
|
.Replace(" ", "")
|
||||||
|
.ReplaceLineBreaks(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ParseProcess(string text)
|
||||||
|
{
|
||||||
|
if (text.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
if (text.StartsWith('"'))
|
||||||
|
{
|
||||||
|
text = text[1..];
|
||||||
|
}
|
||||||
|
if (text.EndsWith('"'))
|
||||||
|
{
|
||||||
|
text = text[..^1];
|
||||||
|
}
|
||||||
|
return List2String(text.Replace(",", ",")
|
||||||
|
.Replace("\\", "/")
|
||||||
|
.ReplaceLineBreaks(",")
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(x => x.TrimEx())
|
||||||
|
.Where(x => x.IsNotEmpty())
|
||||||
|
.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
|
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
|
||||||
@@ -576,6 +599,58 @@ public class Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseRange(string? input, int min, int max, out int from, out int to)
|
||||||
|
{
|
||||||
|
from = to = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var parts = input.Split('-');
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(parts[0], out from))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
to = from;
|
||||||
|
return from >= min && to <= max;
|
||||||
|
}
|
||||||
|
if (parts.Length != 2
|
||||||
|
|| !int.TryParse(parts[0], out from)
|
||||||
|
|| !int.TryParse(parts[1], out to))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return from >= min && to <= max && from <= to;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseMaxSplit(string? input, int min, int max, out int from, out int to)
|
||||||
|
{
|
||||||
|
from = to = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var parts = input.Split('-');
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(parts[0], out from))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
to = from;
|
||||||
|
return from >= min && to <= max;
|
||||||
|
}
|
||||||
|
if (parts.Length != 2
|
||||||
|
|| !int.TryParse(parts[0], out from)
|
||||||
|
|| !int.TryParse(parts[1], out to))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return from >= min && to <= max && from <= to;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsPrivateNetwork(string ip)
|
public static bool IsPrivateNetwork(string ip)
|
||||||
{
|
{
|
||||||
if (IPAddress.TryParse(ip, out var address))
|
if (IPAddress.TryParse(ip, out var address))
|
||||||
@@ -716,6 +791,24 @@ public class Utils
|
|||||||
return (endpoints, connections);
|
return (endpoints, connections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsLocalIP(string ipAddress)
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(ipAddress, out var targetAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||||
|
.Any(ua => ua.Address.Equals(targetAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ContainsInterfaceName(string inInterfaceName)
|
||||||
|
{
|
||||||
|
return NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.Any(ni => ni.Name.Equals(inInterfaceName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Speed Test
|
#endregion Speed Test
|
||||||
|
|
||||||
#region Miscellaneous
|
#region Miscellaneous
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public enum EServerColName
|
|||||||
SubRemarks,
|
SubRemarks,
|
||||||
DelayVal,
|
DelayVal,
|
||||||
SpeedVal,
|
SpeedVal,
|
||||||
|
IpInfo,
|
||||||
|
|
||||||
TodayDown,
|
TodayDown,
|
||||||
TodayUp,
|
TodayUp,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public static class AppEvents
|
|||||||
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
|
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
|
||||||
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
|
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
|
||||||
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
|
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
|
||||||
|
public static readonly EventChannel<bool> HasUpdateNotified = new();
|
||||||
|
|
||||||
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
|
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
|
||||||
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
|
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
|
||||||
|
|||||||
+47
-19
@@ -55,7 +55,7 @@ public class Global
|
|||||||
public const string DnsOutboundTag = "dns";
|
public const string DnsOutboundTag = "dns";
|
||||||
public const string DnsTag = "dns-module";
|
public const string DnsTag = "dns-module";
|
||||||
public const string DirectDnsTag = "direct-dns";
|
public const string DirectDnsTag = "direct-dns";
|
||||||
public const string BalancerTagSuffix = "-round";
|
public const string BalancerTagSuffix = "-balancer";
|
||||||
public const string StreamSecurity = "tls";
|
public const string StreamSecurity = "tls";
|
||||||
public const string StreamSecurityReality = "reality";
|
public const string StreamSecurityReality = "reality";
|
||||||
public const string Loopback = "127.0.0.1";
|
public const string Loopback = "127.0.0.1";
|
||||||
@@ -90,6 +90,9 @@ public class Global
|
|||||||
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
||||||
public const int SpeedTestPageSize = 1000;
|
public const int SpeedTestPageSize = 1000;
|
||||||
public const string LinuxBash = "/bin/bash";
|
public const string LinuxBash = "/bin/bash";
|
||||||
|
public const string StringTrue = "true";
|
||||||
|
public const string StringFalse = "false";
|
||||||
|
public const int SqliteMaxBatchSize = 10000;
|
||||||
|
|
||||||
public const string SingboxDirectDNSTag = "direct_dns";
|
public const string SingboxDirectDNSTag = "direct_dns";
|
||||||
public const string SingboxRemoteDNSTag = "remote_dns";
|
public const string SingboxRemoteDNSTag = "remote_dns";
|
||||||
@@ -149,6 +152,9 @@ public class Global
|
|||||||
public static readonly List<string> SpeedTestUrls =
|
public static readonly List<string> SpeedTestUrls =
|
||||||
[
|
[
|
||||||
@"https://cachefly.cachefly.net/50mb.test",
|
@"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=10000000",
|
||||||
@"https://speed.cloudflare.com/__down?bytes=50000000",
|
@"https://speed.cloudflare.com/__down?bytes=50000000",
|
||||||
@"https://speed.cloudflare.com/__down?bytes=99999999",
|
@"https://speed.cloudflare.com/__down?bytes=99999999",
|
||||||
@@ -157,6 +163,8 @@ public class Global
|
|||||||
public static readonly List<string> SpeedPingTestUrls =
|
public static readonly List<string> SpeedPingTestUrls =
|
||||||
[
|
[
|
||||||
@"https://www.google.com/generate_204",
|
@"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.gstatic.com/generate_204",
|
||||||
@"https://www.apple.com/library/test/success.html",
|
@"https://www.apple.com/library/test/success.html",
|
||||||
@"http://www.msftconnecttest.com/connecttest.txt"
|
@"http://www.msftconnecttest.com/connecttest.txt"
|
||||||
@@ -203,10 +211,18 @@ public class Global
|
|||||||
|
|
||||||
public const string Hysteria2ProtocolShare = "hy2://";
|
public const string Hysteria2ProtocolShare = "hy2://";
|
||||||
|
|
||||||
|
public const string Hysteria2RealmProtocolShare = "hysteria2+realm://";
|
||||||
|
|
||||||
|
public const string Hysteria2HttpRealmProtocolShare = "hysteria2+realm+http://";
|
||||||
|
|
||||||
public const string NaiveHttpsProtocolShare = "naive+https://";
|
public const string NaiveHttpsProtocolShare = "naive+https://";
|
||||||
|
|
||||||
public const string NaiveQuicProtocolShare = "naive+quic://";
|
public const string NaiveQuicProtocolShare = "naive+quic://";
|
||||||
|
|
||||||
|
public const string SOCKS5Protocol = "socks5://";
|
||||||
|
|
||||||
|
public const string SOCKS4Protocol = "socks4://";
|
||||||
|
|
||||||
public static readonly Dictionary<EConfigType, string> ProtocolShares = new()
|
public static readonly Dictionary<EConfigType, string> ProtocolShares = new()
|
||||||
{
|
{
|
||||||
{ EConfigType.VMess, "vmess://" },
|
{ EConfigType.VMess, "vmess://" },
|
||||||
@@ -321,12 +337,12 @@ public class Global
|
|||||||
|
|
||||||
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
|
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
|
||||||
{
|
{
|
||||||
{ "srtp", "header-srtp" },
|
{ "srtp", "srtp" },
|
||||||
{ "utp", "header-utp" },
|
{ "utp", "utp" },
|
||||||
{ "wechat-video", "header-wechat" },
|
{ "wechat-video", "wechat" },
|
||||||
{ "dtls", "header-dtls" },
|
{ "dtls", "dtls" },
|
||||||
{ "wireguard", "header-wireguard" },
|
{ "wireguard", "wireguard" },
|
||||||
{ "dns", "header-dns" }
|
{ "dns", "dns" }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> CoreTypes =
|
public static readonly List<string> CoreTypes =
|
||||||
@@ -395,6 +411,16 @@ public class Global
|
|||||||
""
|
""
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> FragmentPacketsOptions =
|
||||||
|
[
|
||||||
|
"tlshello",
|
||||||
|
"1-1",
|
||||||
|
"1-2",
|
||||||
|
"1-3",
|
||||||
|
"1-4",
|
||||||
|
"1-5"
|
||||||
|
];
|
||||||
|
|
||||||
public static readonly List<string> UserAgent =
|
public static readonly List<string> UserAgent =
|
||||||
[
|
[
|
||||||
"chrome",
|
"chrome",
|
||||||
@@ -412,13 +438,6 @@ public class Global
|
|||||||
"stream-one"
|
"stream-one"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> AllowInsecure =
|
|
||||||
[
|
|
||||||
"true",
|
|
||||||
"false",
|
|
||||||
""
|
|
||||||
];
|
|
||||||
|
|
||||||
public static readonly List<string> DomainStrategy =
|
public static readonly List<string> DomainStrategy =
|
||||||
[
|
[
|
||||||
"AsIs",
|
"AsIs",
|
||||||
@@ -469,10 +488,11 @@ public class Global
|
|||||||
"zh-Hans",
|
"zh-Hans",
|
||||||
"zh-Hant",
|
"zh-Hant",
|
||||||
"en",
|
"en",
|
||||||
"fa-Ir",
|
"fa",
|
||||||
"fr",
|
"fr",
|
||||||
"ru",
|
"ru",
|
||||||
"hu"
|
"hu",
|
||||||
|
"id"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> Alpns =
|
public static readonly List<string> Alpns =
|
||||||
@@ -662,6 +682,14 @@ public class Global
|
|||||||
"mcbe:bedrock.talonmc.net",
|
"mcbe:bedrock.talonmc.net",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> DefaultRealmStunList =
|
||||||
|
[
|
||||||
|
"turn.cloudflare.com:3478",
|
||||||
|
"stun.nextcloud.com:3478",
|
||||||
|
"stun.sip.us:3478",
|
||||||
|
"global.stun.twilio.com:3478",
|
||||||
|
];
|
||||||
|
|
||||||
public static readonly List<string> OutboundTags =
|
public static readonly List<string> OutboundTags =
|
||||||
[
|
[
|
||||||
ProxyTag,
|
ProxyTag,
|
||||||
@@ -676,15 +704,15 @@ public class Global
|
|||||||
{ "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
|
{ "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"] },
|
{ "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"] },
|
{ "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"] },
|
{ "dot.pub", ["1.12.12.12", "120.53.53.53"] },
|
||||||
{ "doh.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.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.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.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"] },
|
{ "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<string> ExpectedIPs =
|
public static readonly List<string> ExpectedIPs =
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ public static class AutoStartupHandler
|
|||||||
task.Settings.RunOnlyIfIdle = false;
|
task.Settings.RunOnlyIfIdle = false;
|
||||||
task.Settings.IdleSettings.StopOnIdleEnd = false;
|
task.Settings.IdleSettings.StopOnIdleEnd = false;
|
||||||
task.Settings.ExecutionTimeLimit = TimeSpan.Zero;
|
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.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest;
|
||||||
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));
|
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public record CoreConfigContextBuilderAllResult(
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges all errors and warnings from the main result and the optional pre-socks result
|
/// Merges all errors and warnings from the main result and the optional pre-socks result
|
||||||
/// into a single <see cref="NodeValidatorResult"/> for unified notification.
|
/// into a single <see cref="NodeValidatorResult" /> for unified notification.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NodeValidatorResult CombinedValidatorResult => new(
|
public NodeValidatorResult CombinedValidatorResult => new(
|
||||||
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
|
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
|
||||||
@@ -28,14 +28,14 @@ public record CoreConfigContextBuilderAllResult(
|
|||||||
public class CoreConfigContextBuilder
|
public class CoreConfigContextBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a <see cref="CoreConfigContext"/> for the given node, resolves its proxy map,
|
/// Builds a <see cref="CoreConfigContext" /> for the given node, resolves its proxy map,
|
||||||
/// and processes outbound nodes referenced by routing rules.
|
/// and processes outbound nodes referenced by routing rules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
|
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
|
||||||
{
|
{
|
||||||
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
||||||
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
|
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
|
||||||
var context = new CoreConfigContext()
|
var context = new CoreConfigContext
|
||||||
{
|
{
|
||||||
Node = node,
|
Node = node,
|
||||||
RunCoreType = runCoreType,
|
RunCoreType = runCoreType,
|
||||||
@@ -61,7 +61,8 @@ public class CoreConfigContextBuilder
|
|||||||
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
|
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
|
||||||
{
|
{
|
||||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
|
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
|
||||||
foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
|
foreach (var ruleItem in rules.Where(ruleItem =>
|
||||||
|
ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
|
||||||
{
|
{
|
||||||
if (ruleItem.OutboundTag.IsNullOrEmpty())
|
if (ruleItem.OutboundTag.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -72,7 +73,8 @@ public class CoreConfigContextBuilder
|
|||||||
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
|
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
|
||||||
if (ruleOutboundNode == null)
|
if (ruleOutboundNode == null)
|
||||||
{
|
{
|
||||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag));
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound,
|
||||||
|
ruleItem.Remarks, ruleItem.OutboundTag));
|
||||||
ruleItem.OutboundTag = Global.ProxyTag;
|
ruleItem.OutboundTag = Global.ProxyTag;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -83,7 +85,8 @@ public class CoreConfigContextBuilder
|
|||||||
if (!ruleNodeValidatorResult.Success)
|
if (!ruleNodeValidatorResult.Success)
|
||||||
{
|
{
|
||||||
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
|
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
|
||||||
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e)));
|
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag,
|
||||||
|
e)));
|
||||||
ruleItem.OutboundTag = Global.ProxyTag;
|
ruleItem.OutboundTag = Global.ProxyTag;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -91,12 +94,48 @@ public class CoreConfigContextBuilder
|
|||||||
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
|
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var appConfig = JsonUtils.DeepCopy(config);
|
||||||
|
var routeExcludeAddressList = new List<string>();
|
||||||
|
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IPNetwork2.Parse(addr);
|
||||||
|
routeExcludeAddressList.Add(addr);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appConfig.TunModeItem.RouteExcludeAddress = routeExcludeAddressList;
|
||||||
|
context = context with { AppConfig = appConfig };
|
||||||
|
}
|
||||||
|
if (!context.AppConfig.CoreBasicItem.SendThrough.IsNullOrEmpty()
|
||||||
|
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
|
||||||
|
{
|
||||||
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsSendThrough));
|
||||||
|
var appConfig = JsonUtils.DeepCopy(config);
|
||||||
|
appConfig.CoreBasicItem.SendThrough = string.Empty;
|
||||||
|
context = context with { AppConfig = appConfig };
|
||||||
|
}
|
||||||
|
if (!context.AppConfig.CoreBasicItem.BindInterface.IsNullOrEmpty()
|
||||||
|
&& (!Utils.ContainsInterfaceName(context.AppConfig.CoreBasicItem.BindInterface)
|
||||||
|
|| !(context.IsTunEnabled || context.IsWindows)))
|
||||||
|
{
|
||||||
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
|
||||||
|
var appConfig = JsonUtils.DeepCopy(config);
|
||||||
|
appConfig.CoreBasicItem.BindInterface = string.Empty;
|
||||||
|
context = context with { AppConfig = appConfig };
|
||||||
|
}
|
||||||
|
|
||||||
return new CoreConfigContextBuilderResult(context, validatorResult);
|
return new CoreConfigContextBuilderResult(context, validatorResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the main <see cref="CoreConfigContext"/> for <paramref name="node"/> and, when
|
/// Builds the main <see cref="CoreConfigContext" /> for <paramref name="node" /> and, when
|
||||||
/// the main build succeeds, also builds the optional pre-socks context required for TUN
|
/// the main build succeeds, also builds the optional pre-socks context required for TUN
|
||||||
/// protection or pre-socks proxy chaining.
|
/// protection or pre-socks proxy chaining.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -118,15 +157,30 @@ public class CoreConfigContextBuilder
|
|||||||
{
|
{
|
||||||
Context = mainResult.Context with
|
Context = mainResult.Context with
|
||||||
{
|
{
|
||||||
IsTunEnabled = false, // main core doesn't handle tun directly when pre-socks is used
|
IsTunEnabled = false,
|
||||||
|
// main core doesn't handle tun directly when pre-socks is used
|
||||||
ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList],
|
ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
if (mainResult.Context.IsTunEnabled
|
||||||
|
&& mainResult.Context.AppConfig.TunModeItem.StrictRoute)
|
||||||
|
{
|
||||||
|
var appConfig = JsonUtils.DeepCopy(mainResult.Context.AppConfig);
|
||||||
|
appConfig.CoreBasicItem.BindInterface = string.Empty;
|
||||||
|
appConfig.CoreBasicItem.SendThrough = string.Empty;
|
||||||
|
resolvedMainResult = resolvedMainResult with
|
||||||
|
{
|
||||||
|
Context = resolvedMainResult.Context with
|
||||||
|
{
|
||||||
|
AppConfig = appConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
|
return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether a pre-socks context is required for <paramref name="nodeContext"/>
|
/// Determines whether a pre-socks context is required for <paramref name="nodeContext" />
|
||||||
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
|
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
|
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
|
||||||
@@ -143,8 +197,9 @@ public class CoreConfigContextBuilder
|
|||||||
{
|
{
|
||||||
Context = preSocksResult.Context with
|
Context = preSocksResult.Context with
|
||||||
{
|
{
|
||||||
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
|
ProtectDomainList =
|
||||||
}
|
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +244,12 @@ public class CoreConfigContextBuilder
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If the node's subscription defines prev/next profiles, creates a virtual
|
/// If the node's subscription defines prev/next profiles, creates a virtual
|
||||||
/// <see cref="EConfigType.ProxyChain"/> node that wraps them together.
|
/// <see cref="EConfigType.ProxyChain" /> node that wraps them together.
|
||||||
/// Returns <c>null</c> as the chain item when no chain is needed.
|
/// Returns <c>null</c> as the chain item when no chain is needed.
|
||||||
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
|
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node)
|
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)>
|
||||||
|
BuildSubscriptionChainNodeAsync(ProfileItem node)
|
||||||
{
|
{
|
||||||
var result = NodeValidatorResult.Empty();
|
var result = NodeValidatorResult.Empty();
|
||||||
|
|
||||||
@@ -234,7 +290,7 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build new proxy chain node
|
// Build new proxy chain node
|
||||||
var chainNode = new ProfileItem()
|
var chainNode = new ProfileItem
|
||||||
{
|
{
|
||||||
IndexId = $"inner-{Utils.GetGuid(false)}",
|
IndexId = $"inner-{Utils.GetGuid(false)}",
|
||||||
ConfigType = EConfigType.ProxyChain,
|
ConfigType = EConfigType.ProxyChain,
|
||||||
@@ -252,8 +308,8 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync"/> or
|
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync" /> or
|
||||||
/// <see cref="RegisterSingleNodeAsync"/> based on the node's config type.
|
/// <see cref="RegisterSingleNodeAsync" /> based on the node's config type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
|
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
|
||||||
{
|
{
|
||||||
@@ -261,11 +317,8 @@ public class CoreConfigContextBuilder
|
|||||||
{
|
{
|
||||||
return await RegisterGroupNodeAsync(context, node);
|
return await RegisterGroupNodeAsync(context, node);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
return RegisterSingleNodeAsync(context, node);
|
return RegisterSingleNodeAsync(context, node);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a single (non-group) node and, on success, adds it to the proxy map
|
/// Validates a single (non-group) node and, on success, adds it to the proxy map
|
||||||
@@ -334,7 +387,7 @@ public class CoreConfigContextBuilder
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entry point for registering a group node. Initialises the visited/ancestor sets
|
/// Entry point for registering a group node. Initialises the visited/ancestor sets
|
||||||
/// and delegates to <see cref="TraverseGroupNodeAsync"/>.
|
/// and delegates to <see cref="TraverseGroupNodeAsync" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
|
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
|
||||||
ProfileItem node)
|
ProfileItem node)
|
||||||
@@ -351,8 +404,8 @@ public class CoreConfigContextBuilder
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively walks the children of a group node, registering valid leaf nodes
|
/// Recursively walks the children of a group node, registering valid leaf nodes
|
||||||
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup"/> and
|
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup" /> and
|
||||||
/// deduplicates shared nodes via <paramref name="globalVisitedGroup"/>.
|
/// deduplicates shared nodes via <paramref name="globalVisitedGroup" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
|
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
|
||||||
CoreConfigContext context,
|
CoreConfigContext context,
|
||||||
@@ -416,13 +469,10 @@ public class CoreConfigContextBuilder
|
|||||||
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
|
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
|
||||||
return childNodeValidatorResult;
|
return childNodeValidatorResult;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
|
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
|
||||||
childNodeValidatorResult.Errors.Clear();
|
childNodeValidatorResult.Errors.Clear();
|
||||||
}
|
|
||||||
|
|
||||||
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), });
|
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList) });
|
||||||
context.AllProxiesMap[node.IndexId] = node;
|
context.AllProxiesMap[node.IndexId] = node;
|
||||||
return childNodeValidatorResult;
|
return childNodeValidatorResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,35 +29,6 @@ public class NodeValidator
|
|||||||
return v.ToResult();
|
return v.ToResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ValidationContext
|
|
||||||
{
|
|
||||||
public List<string> Errors { get; } = [];
|
|
||||||
public List<string> 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)
|
private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
|
||||||
{
|
{
|
||||||
if (item.ConfigType is EConfigType.Custom)
|
if (item.ConfigType is EConfigType.Custom)
|
||||||
@@ -72,8 +43,8 @@ public class NodeValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Basic Property Validation
|
// Basic Property Validation
|
||||||
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address"));
|
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbAddress));
|
||||||
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port"));
|
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, ResUI.TbPort));
|
||||||
|
|
||||||
// Network & Core Logic
|
// Network & Core Logic
|
||||||
var net = item.GetNetwork();
|
var net = item.GetNetwork();
|
||||||
@@ -104,41 +75,76 @@ public class NodeValidator
|
|||||||
{
|
{
|
||||||
case EConfigType.VMess:
|
case EConfigType.VMess:
|
||||||
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
|
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "Password"));
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EConfigType.VLESS:
|
case EConfigType.VLESS:
|
||||||
v.Assert(
|
v.Assert(
|
||||||
!item.Password.IsNullOrEmpty()
|
!item.Password.IsNullOrEmpty()
|
||||||
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
|
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "Password")
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId5)
|
||||||
);
|
);
|
||||||
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
|
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "Flow"));
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbFlow5));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EConfigType.Shadowsocks:
|
case EConfigType.Shadowsocks:
|
||||||
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password"));
|
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbId3));
|
||||||
v.Assert(
|
v.Assert(
|
||||||
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
|
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
|
||||||
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
|
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "SsMethod"));
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbSecurity3));
|
||||||
break;
|
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
|
// TLS & Security
|
||||||
if (item.StreamSecurity == Global.StreamSecurity)
|
if (item.StreamSecurity == Global.StreamSecurity)
|
||||||
{
|
{
|
||||||
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 &&
|
var isCertProvided = !item.Cert.IsNullOrEmpty();
|
||||||
!item.CertSha.IsNullOrEmpty())
|
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0)
|
||||||
{
|
{
|
||||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
|
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFullCertTips));
|
||||||
|
isCertProvided = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
&& !isCertProvided
|
||||||
|
&& item.CertSha.IsNullOrEmpty())
|
||||||
|
|| (coreType == ECoreType.sing_box
|
||||||
|
&& item.GetAllowInsecure()
|
||||||
|
&& !isCertProvided))
|
||||||
|
{
|
||||||
|
v.Warning(ResUI.MsgInsecureConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.StreamSecurity == Global.StreamSecurityReality)
|
if (item.StreamSecurity == Global.StreamSecurityReality)
|
||||||
{
|
{
|
||||||
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey"));
|
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbPublicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
var transport = item.GetTransportExtra();
|
var transport = item.GetTransportExtra();
|
||||||
@@ -146,7 +152,7 @@ public class NodeValidator
|
|||||||
{
|
{
|
||||||
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject)
|
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject)
|
||||||
{
|
{
|
||||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra"));
|
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TransportExtra));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +160,7 @@ public class NodeValidator
|
|||||||
{
|
{
|
||||||
if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject)
|
if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject)
|
||||||
{
|
{
|
||||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "Finalmask"));
|
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFinalmask));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,4 +189,33 @@ public class NodeValidator
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ValidationContext
|
||||||
|
{
|
||||||
|
public List<string> Errors { get; } = [];
|
||||||
|
public List<string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,15 +39,14 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
LogEnabled = false,
|
LogEnabled = false,
|
||||||
Loglevel = "warning",
|
Loglevel = "warning",
|
||||||
MuxEnabled = false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.Inbound == null)
|
if (config.Inbound == null)
|
||||||
{
|
{
|
||||||
config.Inbound = new List<InItem>();
|
config.Inbound = [];
|
||||||
InItem inItem = new()
|
InItem inItem = new()
|
||||||
{
|
{
|
||||||
Protocol = EInboundProtocol.socks.ToString(),
|
Protocol = nameof(EInboundProtocol.socks),
|
||||||
LocalPort = 10808,
|
LocalPort = 10808,
|
||||||
UdpEnabled = true,
|
UdpEnabled = true,
|
||||||
SniffingEnabled = true,
|
SniffingEnabled = true,
|
||||||
@@ -60,7 +59,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
if (config.Inbound.Count > 0)
|
if (config.Inbound.Count > 0)
|
||||||
{
|
{
|
||||||
config.Inbound.First().Protocol = EInboundProtocol.socks.ToString();
|
config.Inbound.First().Protocol = nameof(EInboundProtocol.socks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +98,8 @@ public static class ConfigHandler
|
|||||||
config.MsgUIItem ??= new();
|
config.MsgUIItem ??= new();
|
||||||
|
|
||||||
config.UiItem ??= new();
|
config.UiItem ??= new();
|
||||||
config.UiItem.MainColumnItem ??= new();
|
config.UiItem.MainColumnItem ??= [];
|
||||||
config.UiItem.WindowSizeItem ??= new();
|
config.UiItem.WindowSizeItem ??= [];
|
||||||
|
|
||||||
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
|
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -158,7 +157,7 @@ public static class ConfigHandler
|
|||||||
DownMbps = 100
|
DownMbps = 100
|
||||||
};
|
};
|
||||||
config.ClashUIItem ??= new();
|
config.ClashUIItem ??= new();
|
||||||
config.ClashUIItem.ConnectionsColumnItem ??= new();
|
config.ClashUIItem.ConnectionsColumnItem ??= [];
|
||||||
config.SystemProxyItem ??= new();
|
config.SystemProxyItem ??= new();
|
||||||
config.WebDavItem ??= new();
|
config.WebDavItem ??= new();
|
||||||
config.CheckUpdateItem ??= new();
|
config.CheckUpdateItem ??= new();
|
||||||
@@ -166,9 +165,10 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
Packets = "tlshello",
|
Packets = "tlshello",
|
||||||
Length = "50-100",
|
Length = "50-100",
|
||||||
Interval = "10-20"
|
Interval = "10-20",
|
||||||
|
MaxSplit = "0"
|
||||||
};
|
};
|
||||||
config.GlobalHotkeys ??= new();
|
config.GlobalHotkeys ??= [];
|
||||||
|
|
||||||
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -255,6 +255,7 @@ public static class ConfigHandler
|
|||||||
item.Cert = profileItem.Cert;
|
item.Cert = profileItem.Cert;
|
||||||
item.CertSha = profileItem.CertSha;
|
item.CertSha = profileItem.CertSha;
|
||||||
item.EchConfigList = profileItem.EchConfigList;
|
item.EchConfigList = profileItem.EchConfigList;
|
||||||
|
item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName;
|
||||||
item.Finalmask = profileItem.Finalmask;
|
item.Finalmask = profileItem.Finalmask;
|
||||||
item.ProtoExtra = profileItem.ProtoExtra;
|
item.ProtoExtra = profileItem.ProtoExtra;
|
||||||
item.TransportExtra = profileItem.TransportExtra;
|
item.TransportExtra = profileItem.TransportExtra;
|
||||||
@@ -720,11 +721,50 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
var protocolExtra = profileItem.GetProtocolExtra();
|
||||||
|
profileItem.SetProtocolExtra(protocolExtra with
|
||||||
|
{
|
||||||
|
SalamanderPass = protocolExtra.SalamanderPass?.TrimEx(),
|
||||||
|
HopInterval = protocolExtra.HopInterval?.TrimEx(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!protocolExtra.Hy2RealmUrl.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
var realmResult = HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm);
|
||||||
|
if (!realmResult || realm is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (realm.StunList.Count == 0)
|
||||||
|
{
|
||||||
|
realm = realm with
|
||||||
|
{
|
||||||
|
StunList = Global.DefaultRealmStunList,
|
||||||
|
};
|
||||||
|
}
|
||||||
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||||
{
|
{
|
||||||
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
|
Hy2RealmUrl = realm.ToUri(),
|
||||||
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
|
if (isGecko)
|
||||||
|
{
|
||||||
|
var minPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||||
|
var maxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||||
|
if (minPacketSize <= 0
|
||||||
|
|| minPacketSize > maxPacketSize
|
||||||
|
|| maxPacketSize > 2048)
|
||||||
|
{
|
||||||
|
minPacketSize = 512;
|
||||||
|
maxPacketSize = 1200;
|
||||||
|
}
|
||||||
|
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
GeckoMinPacketSize = minPacketSize.ToString(),
|
||||||
|
GeckoMaxPacketSize = maxPacketSize.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await AddServerCommon(config, profileItem, toFile);
|
await AddServerCommon(config, profileItem, toFile);
|
||||||
|
|
||||||
@@ -874,7 +914,7 @@ public static class ConfigHandler
|
|||||||
profileItem.Fingerprint = string.Empty;
|
profileItem.Fingerprint = string.Empty;
|
||||||
profileItem.Alpn = string.Empty;
|
profileItem.Alpn = string.Empty;
|
||||||
profileItem.Network = string.Empty;
|
profileItem.Network = string.Empty;
|
||||||
profileItem.AllowInsecure = "false";
|
profileItem.AllowInsecure = string.Empty;
|
||||||
if (profileItem.StreamSecurity.IsNullOrEmpty())
|
if (profileItem.StreamSecurity.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
profileItem.StreamSecurity = Global.StreamSecurity;
|
profileItem.StreamSecurity = Global.StreamSecurity;
|
||||||
@@ -923,6 +963,7 @@ public static class ConfigHandler
|
|||||||
Delay = t33?.Delay ?? 0,
|
Delay = t33?.Delay ?? 0,
|
||||||
Speed = t33?.Speed ?? 0,
|
Speed = t33?.Speed ?? 0,
|
||||||
Sort = t33?.Sort ?? 0,
|
Sort = t33?.Sort ?? 0,
|
||||||
|
IpInfo = t33?.IpInfo ?? string.Empty,
|
||||||
TodayDown = (t22?.TodayDown ?? 0).ToString("D16"),
|
TodayDown = (t22?.TodayDown ?? 0).ToString("D16"),
|
||||||
TodayUp = (t22?.TodayUp ?? 0).ToString("D16"),
|
TodayUp = (t22?.TodayUp ?? 0).ToString("D16"),
|
||||||
TotalDown = (t22?.TotalDown ?? 0).ToString("D16"),
|
TotalDown = (t22?.TotalDown ?? 0).ToString("D16"),
|
||||||
@@ -943,6 +984,7 @@ public static class ConfigHandler
|
|||||||
EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(),
|
EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(),
|
||||||
EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(),
|
EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(),
|
||||||
EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).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.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(),
|
||||||
EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(),
|
EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(),
|
||||||
EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(),
|
EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(),
|
||||||
@@ -963,6 +1005,7 @@ public static class ConfigHandler
|
|||||||
EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(),
|
EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(),
|
||||||
EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(),
|
EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(),
|
||||||
EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).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.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(),
|
||||||
EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(),
|
EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(),
|
||||||
EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(),
|
EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(),
|
||||||
@@ -1053,8 +1096,8 @@ public static class ConfigHandler
|
|||||||
return new Tuple<int, int>(0, 0);
|
return new Tuple<int, int>(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ProfileItem> lstKeep = new();
|
List<ProfileItem> lstKeep = [];
|
||||||
List<ProfileItem> lstRemove = new();
|
List<ProfileItem> lstRemove = [];
|
||||||
if (!config.GuiItem.KeepOlderDedupl)
|
if (!config.GuiItem.KeepOlderDedupl)
|
||||||
{
|
{
|
||||||
lstProfile.Reverse();
|
lstProfile.Reverse();
|
||||||
@@ -1103,10 +1146,6 @@ public static class ConfigHandler
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (profileItem.AllowInsecure.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower();
|
|
||||||
}
|
|
||||||
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
|
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
|
||||||
{
|
{
|
||||||
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
|
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
|
||||||
@@ -1451,8 +1490,7 @@ public static class ConfigHandler
|
|||||||
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
|
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
|
||||||
{
|
{
|
||||||
ProfileItem? itemSocks = null;
|
ProfileItem? itemSocks = null;
|
||||||
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect
|
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
|
||||||
|| Utils.IsNonWindows();
|
|
||||||
if (node.ConfigType != EConfigType.Custom
|
if (node.ConfigType != EConfigType.Custom
|
||||||
&& coreType != ECoreType.sing_box
|
&& coreType != ECoreType.sing_box
|
||||||
&& config.TunModeItem.EnableTun
|
&& config.TunModeItem.EnableTun
|
||||||
@@ -1534,7 +1572,7 @@ public static class ConfigHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var countServers = 0;
|
var countServers = 0;
|
||||||
List<ProfileItem> lstAdd = new();
|
List<ProfileItem> lstAdd = [];
|
||||||
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
|
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
|
||||||
if (isSub)
|
if (isSub)
|
||||||
{
|
{
|
||||||
@@ -1630,7 +1668,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
|
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
|
||||||
}
|
}
|
||||||
if (lstProfiles != null && lstProfiles.Count > 0)
|
if (lstProfiles is { Count: > 0 })
|
||||||
{
|
{
|
||||||
var count = 0;
|
var count = 0;
|
||||||
foreach (var it in lstProfiles)
|
foreach (var it in lstProfiles)
|
||||||
|
|||||||
@@ -4,53 +4,41 @@ public static class ConnectionHandler
|
|||||||
{
|
{
|
||||||
private static readonly string _tag = "ConnectionHandler";
|
private static readonly string _tag = "ConnectionHandler";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs ping and IP checks and returns a formatted result string.
|
||||||
|
/// </summary>
|
||||||
public static async Task<string> RunAvailabilityCheck()
|
public static async Task<string> RunAvailabilityCheck()
|
||||||
{
|
{
|
||||||
var time = await GetRealPingTimeInfo();
|
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);
|
return string.Format(ResUI.TestMeOutput, time, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets IP information using the default local proxy.
|
||||||
|
/// </summary>
|
||||||
private static async Task<string?> GetIPInfo()
|
private static async Task<string?> GetIPInfo()
|
||||||
{
|
{
|
||||||
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
|
var webProxy = await GetWebProxy();
|
||||||
if (url.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var downloadHandle = new DownloadService();
|
var ipInfo = await GetIPInfo(webProxy);
|
||||||
var result = await downloadHandle.TryDownloadString(url, true, "");
|
return ipInfo?.ToString() ?? Global.None;
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(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;
|
|
||||||
|
|
||||||
return $"({country ?? "unknown"}) {ip}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measures real ping time using configured test URL.
|
||||||
|
/// </summary>
|
||||||
private static async Task<int> GetRealPingTimeInfo()
|
private static async Task<int> GetRealPingTimeInfo()
|
||||||
{
|
{
|
||||||
var responseTime = -1;
|
var responseTime = -1;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
var webProxy = await GetWebProxy();
|
||||||
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
|
|
||||||
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
|
||||||
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
responseTime = await GetRealPingTime(url, webProxy, 10);
|
responseTime = await GetRealPingTime(webProxy, 10);
|
||||||
if (responseTime > 0)
|
if (responseTime > 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -66,8 +54,21 @@ public static class ConnectionHandler
|
|||||||
return responseTime;
|
return responseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
|
/// <summary>
|
||||||
|
/// Creates local SOCKS proxy instance.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<WebProxy?> GetWebProxy()
|
||||||
{
|
{
|
||||||
|
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||||
|
return new WebProxy($"socks5://{Global.Loopback}:{port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measures response time by sending HTTP requests through proxy.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
|
||||||
|
{
|
||||||
|
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||||
var responseTime = -1;
|
var responseTime = -1;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -79,14 +80,14 @@ public static class ConnectionHandler
|
|||||||
UseProxy = webProxy != null
|
UseProxy = webProxy != null
|
||||||
});
|
});
|
||||||
|
|
||||||
List<int> oneTime = new();
|
List<int> oneTime = [];
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var timer = Stopwatch.StartNew();
|
var timer = Stopwatch.StartNew();
|
||||||
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
|
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
||||||
await Task.Delay(100);
|
await Task.Delay(100, cts.Token);
|
||||||
}
|
}
|
||||||
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
||||||
}
|
}
|
||||||
@@ -95,4 +96,41 @@ public static class ConnectionHandler
|
|||||||
}
|
}
|
||||||
return responseTime;
|
return responseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets IP and country information through specified proxy.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<IpInfoResult?> 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<IPAPIInfo>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ public class BaseFmt
|
|||||||
{
|
{
|
||||||
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
||||||
}
|
}
|
||||||
|
if (item.VerifyPeerCertByName.IsNotEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName));
|
||||||
|
}
|
||||||
if (item.CertSha.IsNotEmpty())
|
if (item.CertSha.IsNotEmpty())
|
||||||
{
|
{
|
||||||
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
|
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
|
||||||
@@ -199,7 +203,7 @@ public class BaseFmt
|
|||||||
|
|
||||||
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
||||||
{
|
{
|
||||||
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
|
if (item.GetAllowInsecure())
|
||||||
{
|
{
|
||||||
// Add two for compatibility
|
// Add two for compatibility
|
||||||
dicQuery.Add("insecure", "1");
|
dicQuery.Add("insecure", "1");
|
||||||
@@ -227,6 +231,7 @@ public class BaseFmt
|
|||||||
item.SpiderX = GetQueryDecoded(query, "spx");
|
item.SpiderX = GetQueryDecoded(query, "spx");
|
||||||
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
|
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
|
||||||
item.EchConfigList = GetQueryDecoded(query, "ech");
|
item.EchConfigList = GetQueryDecoded(query, "ech");
|
||||||
|
item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn");
|
||||||
item.CertSha = GetQueryDecoded(query, "pcs");
|
item.CertSha = GetQueryDecoded(query, "pcs");
|
||||||
|
|
||||||
var finalmaskDecoded = GetQueryDecoded(query, "fm");
|
var finalmaskDecoded = GetQueryDecoded(query, "fm");
|
||||||
@@ -249,11 +254,11 @@ public class BaseFmt
|
|||||||
|
|
||||||
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
|
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
|
||||||
{
|
{
|
||||||
item.AllowInsecure = Global.AllowInsecure.First();
|
item.AllowInsecure = Global.StringTrue;
|
||||||
}
|
}
|
||||||
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
|
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
|
||||||
{
|
{
|
||||||
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
|
item.AllowInsecure = Global.StringFalse;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ public class FmtHandler
|
|||||||
{
|
{
|
||||||
return ShadowsocksFmt.Resolve(str, out msg);
|
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);
|
return SocksFmt.Resolve(str, out msg);
|
||||||
}
|
}
|
||||||
@@ -65,10 +67,16 @@ public class FmtHandler
|
|||||||
{
|
{
|
||||||
return VLESSFmt.Resolve(str, out msg);
|
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);
|
return Hysteria2Fmt.Resolve(str, out msg);
|
||||||
}
|
}
|
||||||
|
else if (str.StartsWith(Global.Hysteria2RealmProtocolShare)
|
||||||
|
|| str.StartsWith(Global.Hysteria2HttpRealmProtocolShare))
|
||||||
|
{
|
||||||
|
return Hysteria2Fmt.ResolveRealm(str, out msg);
|
||||||
|
}
|
||||||
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
|
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
|
||||||
{
|
{
|
||||||
return TuicFmt.Resolve(str, out msg);
|
return TuicFmt.Resolve(str, out msg);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
|
||||||
namespace ServiceLib.Handler.Fmt;
|
namespace ServiceLib.Handler.Fmt;
|
||||||
|
|
||||||
public class Hysteria2Fmt : BaseFmt
|
public class Hysteria2Fmt : BaseFmt
|
||||||
@@ -23,15 +25,7 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
|
|
||||||
var query = Utils.ParseQueryString(url.Query);
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
ResolveUriQuery(query, ref item);
|
ResolveUriQuery(query, ref item);
|
||||||
if (item.CertSha.IsNullOrEmpty())
|
ResolveHy2UriQuery(query, ref item);
|
||||||
{
|
|
||||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
|
||||||
}
|
|
||||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
|
||||||
{
|
|
||||||
Ports = GetQueryDecoded(query, "mport"),
|
|
||||||
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -42,8 +36,11 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
var url = string.Empty;
|
if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return ToUriForRealm(item);
|
||||||
|
}
|
||||||
|
|
||||||
var remark = string.Empty;
|
var remark = string.Empty;
|
||||||
if (item.Remarks.IsNotEmpty())
|
if (item.Remarks.IsNotEmpty())
|
||||||
@@ -52,27 +49,7 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
}
|
}
|
||||||
var dicQuery = new Dictionary<string, string>();
|
var dicQuery = new Dictionary<string, string>();
|
||||||
ToUriQueryLite(item, ref dicQuery);
|
ToUriQueryLite(item, ref dicQuery);
|
||||||
var protocolExtraItem = item.GetProtocolExtra();
|
ToHy2UriQuery(item, ref dicQuery);
|
||||||
|
|
||||||
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
dicQuery.Add("obfs", "salamander");
|
|
||||||
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
|
|
||||||
}
|
|
||||||
if (!protocolExtraItem.Ports.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
|
|
||||||
}
|
|
||||||
if (!item.CertSha.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
var sha = item.CertSha;
|
|
||||||
var idx = sha.IndexOf(',');
|
|
||||||
if (idx > 0)
|
|
||||||
{
|
|
||||||
sha = sha[..idx];
|
|
||||||
}
|
|
||||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
|
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||||
}
|
}
|
||||||
@@ -94,4 +71,155 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ProfileItem? ResolveRealm(string str, out string msg)
|
||||||
|
{
|
||||||
|
msg = ResUI.ConfigurationFormatIncorrect;
|
||||||
|
ProfileItem item = new()
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.Hysteria2
|
||||||
|
};
|
||||||
|
var realmStr = str["hysteria2+".Length..];
|
||||||
|
var result = HyRealm.TryParse(realmStr, out var realm);
|
||||||
|
if (!result || realm == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = Utils.TryUri(str);
|
||||||
|
if (url == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Address = realm.RendezvousHost;
|
||||||
|
item.Port = realm.RendezvousPort;
|
||||||
|
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
|
||||||
|
|
||||||
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
|
ResolveUriQuery(query, ref item);
|
||||||
|
var authPassword = GetQueryDecoded(query, "auth");
|
||||||
|
if (authPassword.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
item.Password = authPassword;
|
||||||
|
ResolveHy2UriQuery(query, ref item);
|
||||||
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
Hy2RealmUrl = realm.ToUri(),
|
||||||
|
});
|
||||||
|
|
||||||
|
msg = string.Empty;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ToUriForRealm(ProfileItem? item)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
|
var result = HyRealm.TryParse(protocolExtraItem.Hy2RealmUrl, out var realm);
|
||||||
|
if (!result || realm == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remark = string.Empty;
|
||||||
|
if (item.Remarks.IsNotEmpty())
|
||||||
|
{
|
||||||
|
remark = "#" + Utils.UrlEncode(item.Remarks);
|
||||||
|
}
|
||||||
|
var dicQuery = new Dictionary<string, string>();
|
||||||
|
ToUriQueryLite(item, ref dicQuery);
|
||||||
|
ToHy2UriQuery(item, ref dicQuery);
|
||||||
|
|
||||||
|
dicQuery.Add("auth", Utils.UrlEncode(item.Password));
|
||||||
|
|
||||||
|
var queryBuilder = new StringBuilder();
|
||||||
|
queryBuilder.Append('?');
|
||||||
|
foreach (var kv in dicQuery)
|
||||||
|
{
|
||||||
|
queryBuilder.Append(kv.Key);
|
||||||
|
queryBuilder.Append('=');
|
||||||
|
queryBuilder.Append(kv.Value);
|
||||||
|
queryBuilder.Append('&');
|
||||||
|
}
|
||||||
|
foreach (var stun in realm.StunList)
|
||||||
|
{
|
||||||
|
queryBuilder.Append("stun=");
|
||||||
|
queryBuilder.Append(Utils.UrlEncode(stun));
|
||||||
|
queryBuilder.Append('&');
|
||||||
|
}
|
||||||
|
var query = queryBuilder.ToString().TrimEnd('&');
|
||||||
|
|
||||||
|
var url = $"{Utils.UrlEncode(realm.Token)}@{GetIpv6(realm.RendezvousHost)}:{realm.RendezvousPort}";
|
||||||
|
var scheme = realm.IsHttp ? Global.Hysteria2HttpRealmProtocolShare : Global.Hysteria2RealmProtocolShare;
|
||||||
|
return $"{scheme}{url}/{realm.RealmName}{query}{remark}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
|
||||||
|
{
|
||||||
|
if (item.CertSha.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
||||||
|
}
|
||||||
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
Ports = GetQueryDecoded(query, "mport"),
|
||||||
|
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
||||||
|
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
|
||||||
|
GeckoMinPacketSize = GetQueryDecoded(query, "minPacketSize"),
|
||||||
|
GeckoMaxPacketSize = GetQueryDecoded(query, "maxPacketSize"),
|
||||||
|
});
|
||||||
|
if (GetQueryDecoded(query, "obfs") == "gecko")
|
||||||
|
{
|
||||||
|
// Ensure the "PacketSize" parameters are present for gecko obfs.
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
|
if (protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
protocolExtraItem = protocolExtraItem with
|
||||||
|
{
|
||||||
|
GeckoMinPacketSize = "512",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
protocolExtraItem = protocolExtraItem with
|
||||||
|
{
|
||||||
|
GeckoMaxPacketSize = "1200",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
item.SetProtocolExtra(protocolExtraItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
||||||
|
{
|
||||||
|
if (!item.CertSha.IsNullOrEmpty()
|
||||||
|
&& !item.CertSha.Contains(','))
|
||||||
|
{
|
||||||
|
var sha = item.CertSha;
|
||||||
|
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||||
|
}
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
|
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
|
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("obfs", isGecko ? "gecko" : "salamander");
|
||||||
|
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
|
||||||
|
if (isGecko)
|
||||||
|
{
|
||||||
|
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
|
||||||
|
dicQuery.Add("minPacketSize", protocolExtraItem.GeckoMinPacketSize);
|
||||||
|
dicQuery.Add("maxPacketSize", protocolExtraItem.GeckoMaxPacketSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!protocolExtraItem.Ports.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ public class InnerFmt
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Check Enum.IsDefined
|
// Check Enum.IsDefined
|
||||||
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
|
if (!Enum.IsDefined(profileItem.ConfigType))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class SocksFmt : BaseFmt
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var arr21 = arr1.First().Split(':');
|
var arr21 = arr1.First().Split(':');
|
||||||
var indexPort = arr1.Last().LastIndexOf(":");
|
var indexPort = arr1.Last().LastIndexOf(':');
|
||||||
if (arr21.Length != 2 || indexPort < 0)
|
if (arr21.Length != 2 || indexPort < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -99,12 +99,17 @@ public class SocksFmt : BaseFmt
|
|||||||
};
|
};
|
||||||
// parse base64 UserInfo
|
// parse base64 UserInfo
|
||||||
var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo);
|
var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo);
|
||||||
var userInfo = Utils.Base64Decode(rawUserInfo);
|
if (rawUserInfo.IsNotEmpty())
|
||||||
var userInfoParts = userInfo.Split([':'], 2);
|
{
|
||||||
|
var userInfoParts = rawUserInfo.Contains(':')
|
||||||
|
? rawUserInfo.Split(":", 2)
|
||||||
|
: Utils.Base64Decode(rawUserInfo).Split(":", 2);
|
||||||
|
|
||||||
if (userInfoParts.Length == 2)
|
if (userInfoParts.Length == 2)
|
||||||
{
|
{
|
||||||
item.Username = userInfoParts.First();
|
item.Username = userInfoParts.First();
|
||||||
item.Password = userInfoParts[1];
|
item.Password = userInfoParts.Last();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ public class VmessFmt : BaseFmt
|
|||||||
sni = item.Sni,
|
sni = item.Sni,
|
||||||
alpn = item.Alpn,
|
alpn = item.Alpn,
|
||||||
fp = item.Fingerprint,
|
fp = item.Fingerprint,
|
||||||
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
|
insecure = item.GetAllowInsecure() ? "1" : "0",
|
||||||
|
vcn = item.VerifyPeerCertByName,
|
||||||
|
pcs = item.CertSha
|
||||||
};
|
};
|
||||||
|
|
||||||
var url = JsonUtils.Serialize(vmessQRCode);
|
var url = JsonUtils.Serialize(vmessQRCode);
|
||||||
@@ -140,7 +142,9 @@ public class VmessFmt : BaseFmt
|
|||||||
item.Sni = Utils.ToString(vmessQRCode.sni);
|
item.Sni = Utils.ToString(vmessQRCode.sni);
|
||||||
item.Alpn = Utils.ToString(vmessQRCode.alpn);
|
item.Alpn = Utils.ToString(vmessQRCode.alpn);
|
||||||
item.Fingerprint = Utils.ToString(vmessQRCode.fp);
|
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;
|
||||||
|
item.VerifyPeerCertByName = Utils.ToString(vmessQRCode.vcn);
|
||||||
|
item.CertSha = Utils.ToString(vmessQRCode.pcs);
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,12 +133,12 @@ public static class SubscriptionHandler
|
|||||||
|
|
||||||
if (!url.Contains("target="))
|
if (!url.Contains("target="))
|
||||||
{
|
{
|
||||||
url += string.Format("&target={0}", item.ConvertTarget);
|
url += $"&target={item.ConvertTarget}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.Contains("config="))
|
if (!url.Contains("config="))
|
||||||
{
|
{
|
||||||
url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault());
|
url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,11 +244,28 @@ public sealed class AppManager
|
|||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ids.Count <= Global.SqliteMaxBatchSize)
|
||||||
|
{
|
||||||
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
||||||
.Where(it => ids.Contains(it.IndexId))
|
.Where(it => ids.Contains(it.IndexId))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var items = new List<ProfileItem>();
|
||||||
|
for (var size = 0; size < ids.Count; size += Global.SqliteMaxBatchSize)
|
||||||
|
{
|
||||||
|
var chunk = ids.Skip(size).Take(Global.SqliteMaxBatchSize).ToList();
|
||||||
|
var chunkItems = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
||||||
|
.Where(it => chunk.Contains(it.IndexId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
items.AddRange(chunkItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
|
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
|
||||||
{
|
{
|
||||||
var items = await GetProfileItemsByIndexIds(indexIds);
|
var items = await GetProfileItemsByIndexIds(indexIds);
|
||||||
@@ -257,18 +274,11 @@ public sealed class AppManager
|
|||||||
|
|
||||||
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
|
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
|
||||||
{
|
{
|
||||||
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
|
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
|
||||||
if (idList.Count == 0)
|
var items = await GetProfileItemsByIndexIds(ids);
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
|
||||||
.Where(it => idList.Contains(it.IndexId))
|
|
||||||
.ToListAsync();
|
|
||||||
var itemMap = items.ToDictionary(it => it.IndexId);
|
var itemMap = items.ToDictionary(it => it.IndexId);
|
||||||
|
|
||||||
return idList.Select(id => itemMap.GetValueOrDefault(id))
|
return ids.Select(itemMap.GetValueOrDefault)
|
||||||
.Where(item => item != null)
|
.Where(item => item != null)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ public class CertPemManager
|
|||||||
{
|
{
|
||||||
private static readonly string _tag = "CertPemManager";
|
private static readonly string _tag = "CertPemManager";
|
||||||
private static readonly Lazy<CertPemManager> _instance = new(() => new());
|
private static readonly Lazy<CertPemManager> _instance = new(() => new());
|
||||||
public static CertPemManager Instance => _instance.Value;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
|
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
|
||||||
@@ -24,21 +23,17 @@ public class CertPemManager
|
|||||||
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
|
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
|
||||||
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
|
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
|
||||||
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
|
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
|
||||||
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
|
|
||||||
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
|
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
|
||||||
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
|
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
|
||||||
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
|
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
|
||||||
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
|
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
|
||||||
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
|
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
|
||||||
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
|
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
|
||||||
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
|
|
||||||
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
|
|
||||||
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
|
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
|
||||||
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
|
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
|
||||||
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
|
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
|
||||||
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
|
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
|
||||||
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
|
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
|
||||||
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
|
|
||||||
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
|
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
|
||||||
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
|
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
|
||||||
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
|
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
|
||||||
@@ -46,10 +41,6 @@ public class CertPemManager
|
|||||||
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
|
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
|
||||||
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
|
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
|
||||||
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services 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
|
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
|
||||||
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
|
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
|
||||||
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
|
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
|
||||||
@@ -62,7 +53,6 @@ public class CertPemManager
|
|||||||
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
|
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
|
||||||
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
|
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
|
||||||
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
|
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
|
||||||
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
|
|
||||||
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
|
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
|
||||||
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
|
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
|
||||||
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
|
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
|
||||||
@@ -116,16 +106,12 @@ public class CertPemManager
|
|||||||
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
|
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
|
||||||
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
|
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
|
||||||
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
|
"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
|
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
|
||||||
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
|
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
|
||||||
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
|
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
|
||||||
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
|
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
|
||||||
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
|
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
|
||||||
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
|
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
|
||||||
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
|
|
||||||
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
|
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
|
||||||
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
|
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
|
||||||
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
|
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
|
||||||
@@ -179,7 +165,6 @@ public class CertPemManager
|
|||||||
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
|
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
|
||||||
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
|
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
|
||||||
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
|
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
|
||||||
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
|
|
||||||
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
|
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
|
||||||
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
|
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
|
||||||
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
|
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
|
||||||
@@ -200,10 +185,13 @@ public class CertPemManager
|
|||||||
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
|
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static CertPemManager Instance => _instance.Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get certificate in PEM format from a server with CA pinning validation
|
/// Get certificate in PEM format from a server with CA pinning validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
|
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
|
||||||
|
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -216,13 +204,13 @@ public class CertPemManager
|
|||||||
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
||||||
|
|
||||||
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
||||||
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
|
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
|
||||||
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
||||||
|
|
||||||
var sslOptions = new SslClientAuthenticationOptions
|
var sslOptions = new SslClientAuthenticationOptions
|
||||||
{
|
{
|
||||||
TargetHost = serverName,
|
TargetHost = serverName,
|
||||||
RemoteCertificateValidationCallback = callback
|
RemoteCertificateValidationCallback = callback,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
||||||
@@ -251,7 +239,8 @@ public class CertPemManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
|
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
|
||||||
|
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||||
{
|
{
|
||||||
var pemList = new List<string>();
|
var pemList = new List<string>();
|
||||||
try
|
try
|
||||||
@@ -265,13 +254,13 @@ public class CertPemManager
|
|||||||
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
||||||
|
|
||||||
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
||||||
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
|
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
|
||||||
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
||||||
|
|
||||||
var sslOptions = new SslClientAuthenticationOptions
|
var sslOptions = new SslClientAuthenticationOptions
|
||||||
{
|
{
|
||||||
TargetHost = serverName,
|
TargetHost = serverName,
|
||||||
RemoteCertificateValidationCallback = callback
|
RemoteCertificateValidationCallback = callback,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
||||||
@@ -303,40 +292,33 @@ public class CertPemManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate server certificate with CA pinning
|
/// Validate server certificate with CA pinning
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool ValidateServerCertificate(
|
private static bool ValidateServerCertificate(
|
||||||
object _,
|
object _,
|
||||||
X509Certificate? certificate,
|
X509Certificate? certificate,
|
||||||
X509Chain? chain,
|
X509Chain? chain,
|
||||||
SslPolicyErrors sslPolicyErrors,
|
SslPolicyErrors sslPolicyErrors,
|
||||||
bool allowInsecure)
|
List<string> verifyPeerCertByName)
|
||||||
{
|
{
|
||||||
if (certificate == null)
|
if (certificate == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In insecure mode, accept any certificate so self-signed certs can be fetched.
|
|
||||||
if (allowInsecure)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check certificate name mismatch
|
|
||||||
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build certificate chain
|
// Build certificate chain
|
||||||
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
|
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
|
||||||
var certChain = chain ?? new X509Chain();
|
var certChain = chain ?? new X509Chain();
|
||||||
|
|
||||||
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
|
if (chain == null)
|
||||||
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
|
{
|
||||||
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
|
certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||||
certChain.ChainPolicy.VerificationTime = DateTime.Now;
|
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||||
|
certChain.ChainPolicy.VerificationTime = DateTime.UtcNow;
|
||||||
|
|
||||||
certChain.Build(cert2);
|
if (!certChain.Build(cert2))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find root CA
|
// Find root CA
|
||||||
if (certChain.ChainElements.Count == 0)
|
if (certChain.ChainElements.Count == 0)
|
||||||
@@ -344,10 +326,37 @@ public class CertPemManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate;
|
var rootCert = certChain.ChainElements[^1].Certificate;
|
||||||
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
|
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)
|
public static string ExportCertToPem(X509Certificate2 cert)
|
||||||
@@ -415,14 +424,9 @@ public class CertPemManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pemList">List of individual PEM certificate strings</param>
|
/// <param name="pemList">List of individual PEM certificate strings</param>
|
||||||
/// <returns>Concatenated PEM certificates string</returns>
|
/// <returns>Concatenated PEM certificates string</returns>
|
||||||
public static string ConcatenatePemChain(IEnumerable<string> pemList)
|
public static string ConcatenatePemChain(IEnumerable<string>? pemList)
|
||||||
{
|
{
|
||||||
if (pemList == null)
|
return pemList == null ? string.Empty : string.Concat(pemList);
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Concat(pemList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
|
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
|
||||||
@@ -431,11 +435,7 @@ public class CertPemManager
|
|||||||
{
|
{
|
||||||
var cert = X509Certificate2.CreateFromPem(pemCert);
|
var cert = X509Certificate2.CreateFromPem(pemCert);
|
||||||
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
|
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
|
||||||
if (includeColon)
|
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
|
||||||
{
|
|
||||||
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
|
|
||||||
}
|
|
||||||
return thumbprint;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,20 +45,15 @@ public sealed class ClashApiManager
|
|||||||
{
|
{
|
||||||
await GetClashProxiesAsync();
|
await GetClashProxiesAsync();
|
||||||
}
|
}
|
||||||
lstProxy = new List<ClashProxyModel>();
|
lstProxy = [];
|
||||||
foreach (var kv in _proxies ?? [])
|
lstProxy.AddRange(from kv in _proxies ?? []
|
||||||
{
|
where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower())
|
||||||
if (Global.notAllowTestType.Contains(kv.Value.type?.ToLower()))
|
select new ClashProxyModel()
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
lstProxy.Add(new ClashProxyModel()
|
|
||||||
{
|
{
|
||||||
Name = kv.Value.name,
|
Name = kv.Value.name,
|
||||||
Type = kv.Value.type?.ToLower(),
|
Type = kv.Value.type?.ToLower(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (lstProxy is not { Count: > 0 })
|
if (lstProxy is not { Count: > 0 })
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
|
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
|
||||||
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
|
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
|
||||||
DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.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",
|
DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip",
|
||||||
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip",
|
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip",
|
||||||
},
|
},
|
||||||
@@ -157,6 +158,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
|
DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
|
||||||
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
|
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
|
||||||
DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.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",
|
DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip",
|
||||||
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
|
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
|
||||||
Match = "Xray",
|
Match = "Xray",
|
||||||
@@ -180,6 +182,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
|
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
|
||||||
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
|
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
|
||||||
DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{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",
|
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
|
||||||
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
|
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
|
||||||
Match = "Mihomo",
|
Match = "Mihomo",
|
||||||
@@ -223,6 +226,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
|
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
|
||||||
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
|
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
|
||||||
DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.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",
|
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
|
||||||
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
|
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
|
||||||
Match = "sing-box",
|
Match = "sing-box",
|
||||||
@@ -314,6 +318,7 @@ public sealed class CoreInfoManager
|
|||||||
names.Add("mihomo-linux-amd64");
|
names.Add("mihomo-linux-amd64");
|
||||||
names.Add("mihomo-linux-arm64");
|
names.Add("mihomo-linux-arm64");
|
||||||
names.Add("mihomo-linux-riscv64");
|
names.Add("mihomo-linux-riscv64");
|
||||||
|
names.Add("mihomo-linux-loong64-abi2");
|
||||||
}
|
}
|
||||||
else if (Utils.IsMacOS())
|
else if (Utils.IsMacOS())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ public class CoreManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
await CoreStart(mainContext);
|
await CoreStart(mainContext);
|
||||||
|
await WaitForProxyPort(preContext);
|
||||||
await CoreStartPreService(preContext);
|
await CoreStartPreService(preContext);
|
||||||
|
|
||||||
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
|
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
|
||||||
@@ -213,6 +214,75 @@ public class CoreManager
|
|||||||
await _updateFunc?.Invoke(notify, msg);
|
await _updateFunc?.Invoke(notify, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
|
||||||
|
{
|
||||||
|
if (preContext is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!preContext.AppConfig.TunModeItem.EnableTun)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
|
||||||
|
var rootToken = rootCts.Token;
|
||||||
|
|
||||||
|
var port = preContext.Node.Port;
|
||||||
|
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
|
||||||
|
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
|
||||||
|
var buf = new byte[2];
|
||||||
|
|
||||||
|
while (!rootToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
using var tcp = new TcpClient();
|
||||||
|
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
|
||||||
|
var linkedToken = linkedCts.Token;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await tcp.ConnectAsync(Global.Loopback, port, linkedToken);
|
||||||
|
var stream = tcp.GetStream();
|
||||||
|
|
||||||
|
await stream.WriteAsync(greeting, linkedToken);
|
||||||
|
|
||||||
|
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
|
||||||
|
|
||||||
|
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
|
||||||
|
if (read == 2 && buf[0] == 0x05)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (!rootToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||||
|
{
|
||||||
|
// Connection refused, proxy not ready yet, wait 50ms before retrying
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(50, rootToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore other exceptions and continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Private
|
#endregion Private
|
||||||
|
|
||||||
#region Process
|
#region Process
|
||||||
@@ -230,7 +300,7 @@ public class CoreManager
|
|||||||
{
|
{
|
||||||
if (mayNeedSudo
|
if (mayNeedSudo
|
||||||
&& _config.TunModeItem.EnableTun
|
&& _config.TunModeItem.EnableTun
|
||||||
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo)
|
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray)
|
||||||
&& Utils.IsNonWindows())
|
&& Utils.IsNonWindows())
|
||||||
{
|
{
|
||||||
_linuxSudo = true;
|
_linuxSudo = true;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public class GroupProfileManager
|
|||||||
|
|
||||||
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo)
|
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo)
|
||||||
{
|
{
|
||||||
return await HasCycle(indexId, extraInfo, new HashSet<string>(), new HashSet<string>());
|
return await HasCycle(indexId, extraInfo, [], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
|
private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
|
||||||
@@ -55,7 +55,7 @@ public class GroupProfileManager
|
|||||||
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
|
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
|
||||||
foreach (var childItem in childItems)
|
foreach (var childItem in childItems)
|
||||||
{
|
{
|
||||||
if (await HasCycle(childItem.IndexId, childItem?.GetProtocolExtra(), visited, stack))
|
if (await HasCycle(childItem.IndexId, childItem.GetProtocolExtra(), visited, stack))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ public class GroupProfileManager
|
|||||||
|
|
||||||
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem)
|
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem)
|
||||||
{
|
{
|
||||||
var protocolExtra = profileItem?.GetProtocolExtra();
|
var protocolExtra = profileItem.GetProtocolExtra();
|
||||||
return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
|
return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public class ProfileExManager
|
|||||||
public async Task ClearAll()
|
public async Task ClearAll()
|
||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
||||||
_lstProfileEx = new();
|
_lstProfileEx = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveTo()
|
public async Task SaveTo()
|
||||||
@@ -150,6 +150,14 @@ public class ProfileExManager
|
|||||||
IndexIdEnqueue(indexId);
|
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)
|
public void SetSort(string indexId, int sort)
|
||||||
{
|
{
|
||||||
var profileEx = GetProfileExItem(indexId);
|
var profileEx = GetProfileExItem(indexId);
|
||||||
@@ -174,6 +182,6 @@ public class ProfileExManager
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return _lstProfileEx.Max(t => t == null ? 0 : t.Sort);
|
return _lstProfileEx.Max(t => t?.Sort ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class StatisticsManager
|
|||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
|
||||||
_serverStatItem = null;
|
_serverStatItem = null;
|
||||||
_lstServerStat = new();
|
_lstServerStat = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveTo()
|
public async Task SaveTo()
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
//Logging.SaveLog("Execute delete expired files");
|
//Logging.SaveLog("Execute delete expired files");
|
||||||
|
|
||||||
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1));
|
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1), "Test");
|
||||||
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
|
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
|
||||||
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
|
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
|
||||||
|
|
||||||
@@ -142,5 +142,10 @@ public class TaskManager
|
|||||||
await _updateFunc?.Invoke(false, msg);
|
await _updateFunc?.Invoke(false, msg);
|
||||||
}
|
}
|
||||||
NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
|
NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
|
||||||
|
|
||||||
|
if (msgs.Count > 0)
|
||||||
|
{
|
||||||
|
AppEvents.HasUpdateNotified.Publish(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ public class CoreBasicItem
|
|||||||
|
|
||||||
public string Loglevel { get; set; }
|
public string Loglevel { get; set; }
|
||||||
|
|
||||||
public bool MuxEnabled { get; set; }
|
|
||||||
|
|
||||||
public bool DefAllowInsecure { get; set; }
|
|
||||||
|
|
||||||
public string DefFingerprint { get; set; }
|
public string DefFingerprint { get; set; }
|
||||||
|
|
||||||
public string DefUserAgent { get; set; }
|
public string DefUserAgent { get; set; }
|
||||||
@@ -21,6 +17,8 @@ public class CoreBasicItem
|
|||||||
|
|
||||||
public bool EnableFragment { get; set; }
|
public bool EnableFragment { get; set; }
|
||||||
|
|
||||||
|
public bool EnableFinalFragment { get; set; }
|
||||||
|
|
||||||
public bool EnableCacheFile4Sbox { get; set; } = true;
|
public bool EnableCacheFile4Sbox { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +146,7 @@ public class TunModeItem
|
|||||||
public bool EnableIPv6Address { get; set; }
|
public bool EnableIPv6Address { get; set; }
|
||||||
public string IcmpRouting { get; set; }
|
public string IcmpRouting { get; set; }
|
||||||
public bool EnableLegacyProtect { get; set; }
|
public bool EnableLegacyProtect { get; set; }
|
||||||
|
public List<string>? RouteExcludeAddress { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -159,6 +158,8 @@ public class SpeedTestItem
|
|||||||
public int MixedConcurrencyCount { get; set; }
|
public int MixedConcurrencyCount { get; set; }
|
||||||
public string IPAPIUrl { get; set; }
|
public string IPAPIUrl { get; set; }
|
||||||
public string UdpTestTarget { get; set; }
|
public string UdpTestTarget { get; set; }
|
||||||
|
public int? SpeedTestPageSize { get; set; }
|
||||||
|
public int? SpeedTestDelayInterval { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -248,6 +249,7 @@ public class Fragment4RayItem
|
|||||||
public string? Packets { get; set; }
|
public string? Packets { get; set; }
|
||||||
public string? Length { get; set; }
|
public string? Length { get; set; }
|
||||||
public string? Interval { get; set; }
|
public string? Interval { get; set; }
|
||||||
|
public string? MaxSplit { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class CoreInfo
|
|||||||
public string? DownloadUrlLinux64 { get; set; }
|
public string? DownloadUrlLinux64 { get; set; }
|
||||||
public string? DownloadUrlLinuxArm64 { get; set; }
|
public string? DownloadUrlLinuxArm64 { get; set; }
|
||||||
public string? DownloadUrlLinuxRiscV64 { get; set; }
|
public string? DownloadUrlLinuxRiscV64 { get; set; }
|
||||||
|
public string? DownloadUrlLinuxLoong64 { get; set; }
|
||||||
public string? DownloadUrlOSX64 { get; set; }
|
public string? DownloadUrlOSX64 { get; set; }
|
||||||
public string? DownloadUrlOSXArm64 { get; set; }
|
public string? DownloadUrlOSXArm64 { get; set; }
|
||||||
public string? Match { get; set; }
|
public string? Match { get; set; }
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ public class Rule4Sbox
|
|||||||
public List<string>? wifi_bssid { get; set; }
|
public List<string>? wifi_bssid { get; set; }
|
||||||
public bool? rule_set_ip_cidr_match_source { get; set; }
|
public bool? rule_set_ip_cidr_match_source { get; set; }
|
||||||
public bool? rule_set_ip_cidr_accept_empty { get; set; }
|
public bool? rule_set_ip_cidr_accept_empty { get; set; }
|
||||||
|
public bool? tls_record_fragment { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -110,6 +111,7 @@ public class Inbound4Sbox
|
|||||||
public bool? endpoint_independent_nat { get; set; }
|
public bool? endpoint_independent_nat { get; set; }
|
||||||
public string? stack { get; set; }
|
public string? stack { get; set; }
|
||||||
public List<User4Sbox> users { get; set; }
|
public List<User4Sbox> users { get; set; }
|
||||||
|
public List<string>? route_exclude_address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class User4Sbox
|
public class User4Sbox
|
||||||
@@ -246,6 +248,16 @@ public class HyObfs4Sbox
|
|||||||
{
|
{
|
||||||
public string? type { get; set; }
|
public string? type { get; set; }
|
||||||
public string? password { get; set; }
|
public string? password { get; set; }
|
||||||
|
public int? min_packet_size { get; set; }
|
||||||
|
public int? max_packet_size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HyRealm4Sbox
|
||||||
|
{
|
||||||
|
public string? server_url { get; set; }
|
||||||
|
public string? token { get; set; }
|
||||||
|
public string? realm_id { get; set; }
|
||||||
|
public List<string>? stun_servers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Server4Sbox : BaseServer4Sbox
|
public class Server4Sbox : BaseServer4Sbox
|
||||||
@@ -332,6 +344,7 @@ public abstract class DialFields4Sbox
|
|||||||
public Multiplex4Sbox? multiplex { get; set; }
|
public Multiplex4Sbox? multiplex { get; set; }
|
||||||
public Transport4Sbox? transport { get; set; }
|
public Transport4Sbox? transport { get; set; }
|
||||||
public HyObfs4Sbox? obfs { get; set; }
|
public HyObfs4Sbox? obfs { get; set; }
|
||||||
|
public HyRealm4Sbox? realm { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class BaseServer4Sbox : DialFields4Sbox
|
public abstract class BaseServer4Sbox : DialFields4Sbox
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class Stats4Ray
|
|||||||
|
|
||||||
public class Metrics4Ray
|
public class Metrics4Ray
|
||||||
{
|
{
|
||||||
public string tag { get; set; }
|
public string listen { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Policy4Ray
|
public class Policy4Ray
|
||||||
@@ -86,7 +86,7 @@ public class Inboundsettings4Ray
|
|||||||
|
|
||||||
public string? autoOutboundsInterface { get; set; }
|
public string? autoOutboundsInterface { get; set; }
|
||||||
|
|
||||||
// public List<string>? dns { get; set; }
|
public List<string>? dns { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UsersItem4Ray
|
public class UsersItem4Ray
|
||||||
@@ -136,7 +136,7 @@ public class Outboundsettings4Ray
|
|||||||
|
|
||||||
public Response4Ray? response { get; set; }
|
public Response4Ray? response { get; set; }
|
||||||
|
|
||||||
public string domainStrategy { get; set; }
|
public string? domainStrategy { get; set; }
|
||||||
|
|
||||||
public int? userLevel { get; set; }
|
public int? userLevel { get; set; }
|
||||||
|
|
||||||
@@ -276,6 +276,7 @@ public class BalancersItem4Ray
|
|||||||
public List<string>? selector { get; set; }
|
public List<string>? selector { get; set; }
|
||||||
public BalancersStrategy4Ray? strategy { get; set; }
|
public BalancersStrategy4Ray? strategy { get; set; }
|
||||||
public string? tag { get; set; }
|
public string? tag { get; set; }
|
||||||
|
public string? fallbackTag { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BalancersStrategy4Ray
|
public class BalancersStrategy4Ray
|
||||||
@@ -288,16 +289,7 @@ public class BalancersStrategySettings4Ray
|
|||||||
{
|
{
|
||||||
public int? expected { get; set; }
|
public int? expected { get; set; }
|
||||||
public string? maxRTT { get; set; }
|
public string? maxRTT { get; set; }
|
||||||
public float? tolerance { get; set; }
|
public double? tolerance { get; set; }
|
||||||
public List<string>? baselines { get; set; }
|
|
||||||
public List<BalancersStrategySettingsCosts4Ray>? costs { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BalancersStrategySettingsCosts4Ray
|
|
||||||
{
|
|
||||||
public bool? regexp { get; set; }
|
|
||||||
public string? match { get; set; }
|
|
||||||
public float? value { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Observatory4Ray
|
public class Observatory4Ray
|
||||||
@@ -372,6 +364,7 @@ public class TlsSettings4Ray
|
|||||||
public string? spiderX { get; set; }
|
public string? spiderX { get; set; }
|
||||||
public string? mldsa65Verify { get; set; }
|
public string? mldsa65Verify { get; set; }
|
||||||
public List<CertificateSettings4Ray>? certificates { get; set; }
|
public List<CertificateSettings4Ray>? certificates { get; set; }
|
||||||
|
public string? verifyPeerCertByName { get; set; }
|
||||||
public string? pinnedPeerCertSha256 { get; set; }
|
public string? pinnedPeerCertSha256 { get; set; }
|
||||||
public bool? disableSystemRoot { get; set; }
|
public bool? disableSystemRoot { get; set; }
|
||||||
public string? echConfigList { get; set; }
|
public string? echConfigList { get; set; }
|
||||||
@@ -487,6 +480,7 @@ public class UdpHop4Ray
|
|||||||
|
|
||||||
public class Finalmask4Ray
|
public class Finalmask4Ray
|
||||||
{
|
{
|
||||||
|
public List<Mask4Ray>? tcp { get; set; }
|
||||||
public List<Mask4Ray>? udp { get; set; }
|
public List<Mask4Ray>? udp { get; set; }
|
||||||
public QuicParams4Ray? quicParams { get; set; }
|
public QuicParams4Ray? quicParams { get; set; }
|
||||||
}
|
}
|
||||||
@@ -499,14 +493,21 @@ public class Mask4Ray
|
|||||||
|
|
||||||
public class MaskSettings4Ray
|
public class MaskSettings4Ray
|
||||||
{
|
{
|
||||||
|
public string? header { get; set; }
|
||||||
|
public string? value { get; set; }
|
||||||
|
|
||||||
public string? password { get; set; }
|
public string? password { get; set; }
|
||||||
public string? domain { get; set; }
|
|
||||||
|
public string? url { get; set; }
|
||||||
|
public List<string>? stunServers { get; set; }
|
||||||
|
public string? packetSize { get; set; }
|
||||||
|
|
||||||
// fragment
|
// fragment
|
||||||
public string? packets { get; set; }
|
public string? packets { get; set; }
|
||||||
|
|
||||||
public string? length { get; set; }
|
public string? length { get; set; }
|
||||||
public string? delay { get; set; }
|
public string? delay { get; set; }
|
||||||
|
public int? maxSplit { get; set; }
|
||||||
|
|
||||||
// noise
|
// noise
|
||||||
public int? reset { get; set; }
|
public int? reset { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
|
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||||
|
// realm+http://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||||
|
// example:
|
||||||
|
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
|
||||||
|
public record HyRealm(
|
||||||
|
bool IsHttp,
|
||||||
|
string Token,
|
||||||
|
string RendezvousHost,
|
||||||
|
int RendezvousPort,
|
||||||
|
string RealmName,
|
||||||
|
List<string> StunList
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// sing-box realm.server_url requires a scheme (https:// or http://).
|
||||||
|
/// </summary>
|
||||||
|
public string ToServerUrl()
|
||||||
|
{
|
||||||
|
var scheme = IsHttp ? "http" : "https";
|
||||||
|
return $"{scheme}://{RendezvousHostPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParse(string? str, out HyRealm? realm)
|
||||||
|
{
|
||||||
|
realm = null;
|
||||||
|
if (str == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var isHttp = str.StartsWith("realm+http://");
|
||||||
|
var prefix = isHttp ? "realm+http://" : "realm://";
|
||||||
|
if (!str.StartsWith(prefix))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var uri = new Uri(str);
|
||||||
|
var token = Utils.UrlDecode(uri.UserInfo);
|
||||||
|
var rendezvousHost = uri.Host;
|
||||||
|
var rendezvousPort = uri.IsDefaultPort ? (isHttp ? 80 : 443) : uri.Port;
|
||||||
|
var realmName = uri.AbsolutePath.TrimStart('/');
|
||||||
|
var stunList = new List<string>();
|
||||||
|
var query = uri.Query;
|
||||||
|
if (!query.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
if (query.StartsWith('?'))
|
||||||
|
{
|
||||||
|
query = query.Substring(1);
|
||||||
|
}
|
||||||
|
var pairs = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var pair in pairs)
|
||||||
|
{
|
||||||
|
var idx = pair.IndexOf('=');
|
||||||
|
if (idx <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var key = pair.Substring(0, idx);
|
||||||
|
if (!key.Equals("stun", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var val = pair.Substring(idx + 1);
|
||||||
|
stunList.Add(Utils.UrlDecode(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
realm = new HyRealm(isHttp, token, rendezvousHost, rendezvousPort, realmName, stunList);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToUri()
|
||||||
|
{
|
||||||
|
var prefix = IsHttp ? "realm+http://" : "realm://";
|
||||||
|
var uriBuilder = new UriBuilder
|
||||||
|
{
|
||||||
|
Scheme = IsHttp ? "http" : "realm",
|
||||||
|
Host = RendezvousHost,
|
||||||
|
Port = RendezvousPort,
|
||||||
|
Path = "/" + RealmName,
|
||||||
|
UserName = Utils.UrlEncode(Token),
|
||||||
|
};
|
||||||
|
if (StunList is { Count: > 0 })
|
||||||
|
{
|
||||||
|
// var query = string.Join('&', StunList.Select(s => "stun=" + Utils.UrlEncode(s)));
|
||||||
|
// NOTE: maybe we don't need to encode the stun host:port, since it should be a valid URI component already, and encoding will make it unreadable
|
||||||
|
var query = string.Join('&', StunList.Select(s => "stun=" + s));
|
||||||
|
uriBuilder.Query = query;
|
||||||
|
}
|
||||||
|
return prefix + uriBuilder.Uri.GetComponents(
|
||||||
|
UriComponents.UserInfo | UriComponents.HostAndPort | UriComponents.PathAndQuery, UriFormat.UriEscaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToUriForFinalmask()
|
||||||
|
{
|
||||||
|
var prefix = IsHttp ? "realm+http://" : "realm://";
|
||||||
|
var uriBuilder = new StringBuilder();
|
||||||
|
uriBuilder.Append(prefix);
|
||||||
|
uriBuilder.Append(Utils.UrlEncode(Token));
|
||||||
|
uriBuilder.Append('@');
|
||||||
|
uriBuilder.Append(RendezvousHostPort);
|
||||||
|
uriBuilder.Append('/');
|
||||||
|
uriBuilder.Append(Utils.UrlEncode(RealmName));
|
||||||
|
return uriBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,3 +17,12 @@ public class LocationInfo
|
|||||||
{
|
{
|
||||||
public string? country_code { get; set; }
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject
|
|||||||
[Reactive]
|
[Reactive]
|
||||||
public string SpeedVal { get; set; }
|
public string SpeedVal { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string IpInfo { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string TodayUp { get; set; }
|
public string TodayUp { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
namespace ServiceLib.Models.Dto;
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
public class RetResult
|
public class RetResult(bool success, string? msg, object? data)
|
||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public RetResult(bool success = false) : this(success, null, null)
|
||||||
public string? Msg { get; set; }
|
|
||||||
public object? Data { get; set; }
|
|
||||||
|
|
||||||
public RetResult(bool success = false)
|
|
||||||
{
|
{
|
||||||
Success = success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RetResult(bool success, string? msg)
|
public RetResult(bool success, string? msg) : this(success, msg, null)
|
||||||
{
|
{
|
||||||
Success = success;
|
|
||||||
Msg = msg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RetResult(bool success, string? msg, object? data)
|
public bool Success { get; set; } = success;
|
||||||
{
|
public string? Msg { get; set; } = msg;
|
||||||
Success = success;
|
public object? Data { get; set; } = data;
|
||||||
Msg = msg;
|
|
||||||
Data = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ public class SpeedTestResult
|
|||||||
public string? Delay { get; set; }
|
public string? Delay { get; set; }
|
||||||
|
|
||||||
public string? Speed { get; set; }
|
public string? Speed { get; set; }
|
||||||
|
|
||||||
|
public string? IpInfo { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,8 @@ public class VmessQRCode
|
|||||||
public string fp { get; set; } = string.Empty;
|
public string fp { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string insecure { get; set; } = string.Empty;
|
public string insecure { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string vcn { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string pcs { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ public class ProfileExItem
|
|||||||
public decimal Speed { get; set; }
|
public decimal Speed { get; set; }
|
||||||
public int Sort { get; set; }
|
public int Sort { get; set; }
|
||||||
public string? Message { get; set; }
|
public string? Message { get; set; }
|
||||||
|
public string? IpInfo { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,11 @@ public class ProfileItem
|
|||||||
TransportExtra = JsonUtils.Serialize(transportExtra, false);
|
TransportExtra = JsonUtils.Serialize(transportExtra, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool GetAllowInsecure()
|
||||||
|
{
|
||||||
|
return AllowInsecure == Global.StringTrue;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion function
|
#endregion function
|
||||||
|
|
||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
@@ -191,6 +196,7 @@ public class ProfileItem
|
|||||||
public string Cert { get; set; }
|
public string Cert { get; set; }
|
||||||
public string CertSha { get; set; }
|
public string CertSha { get; set; }
|
||||||
public string EchConfigList { get; set; }
|
public string EchConfigList { get; set; }
|
||||||
|
public string VerifyPeerCertByName { get; set; }
|
||||||
public string Finalmask { get; set; }
|
public string Finalmask { get; set; }
|
||||||
|
|
||||||
public string ProtoExtra { get; set; }
|
public string ProtoExtra { get; set; }
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ public record ProtocolExtraItem
|
|||||||
public int? DownMbps { get; init; }
|
public int? DownMbps { get; init; }
|
||||||
public string? Ports { get; init; }
|
public string? Ports { get; init; }
|
||||||
public string? HopInterval { get; init; }
|
public string? HopInterval { get; init; }
|
||||||
|
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||||
|
// example:
|
||||||
|
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
|
||||||
|
public string? Hy2RealmUrl { get; init; }
|
||||||
|
public string? GeckoMinPacketSize { get; init; }
|
||||||
|
public string? GeckoMaxPacketSize { get; init; }
|
||||||
|
|
||||||
// naiveproxy
|
// naiveproxy
|
||||||
public int? InsecureConcurrency { get; init; }
|
public int? InsecureConcurrency { get; init; }
|
||||||
|
|||||||
Generated
+244
-28
@@ -222,15 +222,6 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string FillCorrectSendThroughIPv4 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Please enter the correct port format. 的本地化字符串。
|
/// 查找类似 Please enter the correct port format. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -240,6 +231,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid range format. Use 'from-to' (e.g., 50-100). 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string FillFragmentParameterError {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FillFragmentParameterError", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Please enter the local listening port. 的本地化字符串。
|
/// 查找类似 Please enter the local listening port. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid Realm URL. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string InvalidHy2RealmUrl {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -564,6 +573,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 IP Info 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string LvTestIpInfo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LvTestIpInfo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Speed (MB/s) 的本地化字符串。
|
/// 查找类似 Speed (MB/s) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1311,6 +1329,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 New Update 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string menuNewUpdate {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuNewUpdate", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Open the storage location 的本地化字符串。
|
/// 查找类似 Open the storage location 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1896,6 +1923,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 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. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgAllowInsecureDeprecated {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgAllowInsecureDeprecated", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
|
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2040,6 +2076,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgInsecureConfiguration {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgInsecureConfiguration", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
|
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2085,6 +2130,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgOptionsConflict {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Resolved {0} successfully 的本地化字符串。
|
/// 查找类似 Resolved {0} successfully 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2202,6 +2256,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgTunRouteExcludeInvalidAddress {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Unpacking... 的本地化字符串。
|
/// 查找类似 Unpacking... 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2625,24 +2688,6 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Allow insecure cert fetch (self-signed) 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string TbAllowInsecureCertFetch {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("TbAllowInsecureCertFetch", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Only for fetching self-signed certificates. This may expose you to MITM risks. 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string TbAllowInsecureCertFetchTips {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("TbAllowInsecureCertFetchTips", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 ALPN 的本地化字符串。
|
/// 查找类似 ALPN 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2988,6 +3033,33 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Enable Final Fragment 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnableFinalFragment {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnableFinalFragment", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Split the tail of packets into smaller fragments. This may affect throughput and latency. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnableFinalFragmentTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnableFinalFragmentTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 DNS via Bridge 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnabletDnsViaProxy {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnabletDnsViaProxy", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Enable Tun 的本地化字符串。
|
/// 查找类似 Enable Tun 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3096,6 +3168,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbGeckoPacketSize {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Global Hotkey Settings 的本地化字符串。
|
/// 查找类似 Global Hotkey Settings 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3159,6 +3240,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Realm URL 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbHy2RealmUrl {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbHy2RealmUrl", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port] 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbHy2RealmUrlTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbHy2RealmUrlTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 ICMP routing policy 的本地化字符串。
|
/// 查找类似 ICMP routing policy 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3510,6 +3609,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Route Exclude Address 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbRouteExcludeAddress {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Use commas (,) to separate. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbRouteExcludeAddressTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
|
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3745,7 +3862,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。
|
/// 查找类似 For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string TbSettingsBindInterfaceTip {
|
public static string TbSettingsBindInterfaceTip {
|
||||||
get {
|
get {
|
||||||
@@ -4032,6 +4149,96 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fallback Delay 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentFallbackDelay {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentFallbackDelayTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment Interval 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentInterval {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be <= second. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentIntervalTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment Length 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentLength {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be <= second. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentLengthTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Max Split 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentMaxSplit {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentMaxSplitTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment Packets 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentPackets {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentPacketsTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Geo files source (optional) 的本地化字符串。
|
/// 查找类似 Geo files source (optional) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4680,6 +4887,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbVerifyPeerCertByName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
|
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1104,6 +1104,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>فعال کردن فرگمنت</value>
|
<value>فعال کردن فرگمنت</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>پکتهای فرگمنت</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>پکتهای قابل فرگمنت: tlshello (TLS ClientHello) یا 1-1 تا 1-5 (N اول پکت TCP)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>طول فرگمنت</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>محدوده اندازه فرگمنت بایت (مثلا 50-100). اول ≤ دوم. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>فاصله فرگمنت</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>تأخیر بین فرگمنتها میلیثانیه (مثلا 10-20). بازه 1-100. اول ≤ دوم. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>حداکثر تقسیم</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>مکسیمم تعداد تقسیم (0 = نامحدود، 0-10000). فقط Xray-core. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>تأخیر فولبک</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>تأخیر فولبک فرگمنت TLS. فقط sing-box. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>فرمت محدوده نامعتبر. از 'from-to' استفاده کنید (مثلا 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
|
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Test Configurations UDP Delay</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Bind Interface</value>
|
<value>Bind Interface</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1740,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Not Support</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>IP Info</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>New Update</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1101,6 +1101,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Activer le fragmentation (Fragment)</value>
|
<value>Activer le fragmentation (Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Paquets fragmentés</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Paquets à fragmenter : tlshello (TLS ClientHello) ou 1-1 à 1-5 (premiers N paquets TCP)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Longueur fragment</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Plage de taille fragment (ex: 50-100). Première valeur ≤ seconde. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Intervalle fragment</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Délai entre fragments en ms (ex: 10-20). Plage 1-100. Première valeur ≤ seconde. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Max Split</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Nb max de splits (0 = illimité, 0-10000). Xray-core seulement. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Délai fallback</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>Délai fallback pour fragment TLS. sing-box seulement. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Format de plage invalide. Utilisez 'from-to' (ex: 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Activer le fichier de cache de sing-box (fichiers règles)</value>
|
<value>Activer le fichier de cache de sing-box (fichiers règles)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1675,7 +1708,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<value>Ajouter [NaïveProxy]</value>
|
<value>Ajouter [NaïveProxy]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbInsecureConcurrency" xml:space="preserve">
|
<data name="TbInsecureConcurrency" xml:space="preserve">
|
||||||
<value>Insecure Concurrency</value>
|
<value>Concurrence non sûre</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbUsername" xml:space="preserve">
|
<data name="TbUsername" xml:space="preserve">
|
||||||
<value>Nom d’utilisateur</value>
|
<value>Nom d’utilisateur</value>
|
||||||
@@ -1695,21 +1728,12 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||||
<value>Adresse sortante locale (SendThrough)</value>
|
<value>Adresse de sortie locale (SendThrough)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
|
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Lier l'interface</value>
|
<value>Lier l'interface</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1717,24 +1741,30 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
|
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Tester la latence UDP des profils</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||||
<value>UDP Test Url</value>
|
<value>URL de test UDP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
<value>Exporter le lien interne v2rayN vers le presse-papiers</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
||||||
<value>{0} has a new version available: {1}</value>
|
<value>{0} a une nouvelle version disponible: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckOnly" xml:space="preserve">
|
<data name="menuCheckOnly" xml:space="preserve">
|
||||||
<value>Only Check</value>
|
<value>Uniquement vérifier</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Non supporté</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>Détails IP</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>Nouvelle MAJ</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1104,6 +1104,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Fragment engedélyezése</value>
|
<value>Fragment engedélyezése</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Fragmentált csomagok</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Fragmentálandó csomagok: tlshello (TLS ClientHello) vagy 1-1 – 1-5 (az első N TCP csomag)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Fragment hossz</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Fragment méret tartománya bájtokban (pl. 50-100). Az első érték ≤ második. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Fragment intervallum</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Késleltetés a fragmented csomagok között ms-ben (pl. 10-20). Tartomány 1-100. Az első érték ≤ második. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Max darabolás</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Maximális darabolások száma (0 = korlátlan, 0-10000). Csak Xray-core. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Fallback késleltetés</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>TLS fragment fallback késleltetés. Csak sing-box. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Érvénytelen tartomány formátum. Használja 'from-to' (pl. 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
|
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Test Configurations UDP Delay</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Bind Interface</value>
|
<value>Bind Interface</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1740,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Not Support</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>IP Info</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>New Update</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1104,6 +1104,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Enable fragment</value>
|
<value>Enable fragment</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Fragment Packets</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Fragment Length</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Fragment size range in bytes (e.g., 50-100). First value must be <= second. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Fragment Interval</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be <= second. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Max Split</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Fallback Delay</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Invalid range format. Use 'from-to' (e.g., 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Enable cache file for sing-box (ruleset files)</value>
|
<value>Enable cache file for sing-box (ruleset files)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1581,6 +1614,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbEchConfigList" xml:space="preserve">
|
<data name="TbEchConfigList" xml:space="preserve">
|
||||||
<value>EchConfigList</value>
|
<value>EchConfigList</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
|
<value>Verify Peer Cert By Name</value>
|
||||||
|
</data>
|
||||||
<data name="TbFullCertTips" xml:space="preserve">
|
<data name="TbFullCertTips" xml:space="preserve">
|
||||||
<value>Full certificate (chain), PEM format</value>
|
<value>Full certificate (chain), PEM format</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1698,12 +1734,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Test Configurations UDP Delay</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,14 +1746,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Bind Interface</value>
|
<value>Bind Interface</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
|
<value>For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
@@ -1740,4 +1767,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Not Support</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>IP Info</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>New Update</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
|
<value>Route Exclude Address</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||||
|
<value>Use commas (,) to separate.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||||
|
<value>Invalid address in TUN route exclude list: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgOptionsConflict" xml:space="preserve">
|
||||||
|
<value>Conflict between {0} and {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragment" xml:space="preserve">
|
||||||
|
<value>Enable Final Fragment</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
|
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
|
<value>DNS via Bridge</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||||
|
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Realm URL</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Invalid Realm URL.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||||
|
<value>Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||||
|
<value>Gecko Packet Size (min/max)</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<value>Ссылка успешно скопирована в буфер обмена</value>
|
<value>Ссылка успешно скопирована в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CheckServerSettings" xml:space="preserve">
|
<data name="CheckServerSettings" xml:space="preserve">
|
||||||
<value>Сначала проверьте настройки сервера</value>
|
<value>Недопустимая конфигурация, проверьте или выберите заново</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
||||||
<value>Недопустимый формат конфигурации</value>
|
<value>Недопустимый формат конфигурации</value>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
<value>Некорректная конфигурация. Проверьте</value>
|
<value>Некорректная конфигурация. Проверьте</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InitialConfiguration" xml:space="preserve">
|
<data name="InitialConfiguration" xml:space="preserve">
|
||||||
<value>Исходная конфигурация</value>
|
<value>Инициализация конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IsLatestCore" xml:space="preserve">
|
<data name="IsLatestCore" xml:space="preserve">
|
||||||
<value>{0} {1} является последней версией</value>
|
<value>{0} {1} является последней версией</value>
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
<value>Операция не удалась, проверьте и повторите попытку</value>
|
<value>Операция не удалась, проверьте и повторите попытку</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseFillRemarks" xml:space="preserve">
|
<data name="PleaseFillRemarks" xml:space="preserve">
|
||||||
<value>Введите примечания</value>
|
<value>Введите псевдоним</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseSelectEncryption" xml:space="preserve">
|
<data name="PleaseSelectEncryption" xml:space="preserve">
|
||||||
<value>Выберите метод шифрования</value>
|
<value>Выберите метод шифрования</value>
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
<value>{0}: одно из обязательных полей</value>
|
<value>{0}: одно из обязательных полей</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvRemarks" xml:space="preserve">
|
<data name="LvRemarks" xml:space="preserve">
|
||||||
<value>Примечания</value>
|
<value>Псевдоним</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvUrl" xml:space="preserve">
|
<data name="LvUrl" xml:space="preserve">
|
||||||
<value>URL (необязательно)</value>
|
<value>URL (необязательно)</value>
|
||||||
@@ -334,13 +334,13 @@
|
|||||||
<value>Введите корректный пользовательский DNS</value>
|
<value>Введите корректный пользовательский DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip1" xml:space="preserve">
|
<data name="TransportPathTip1" xml:space="preserve">
|
||||||
<value>*Путь ws/http upgrade/xhttp</value>
|
<value>*Путь ws/httpupgrade/xhttp</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip2" xml:space="preserve">
|
<data name="TransportPathTip2" xml:space="preserve">
|
||||||
<value>*HTTP2-путь</value>
|
<value>*HTTP2-путь</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip3" xml:space="preserve">
|
<data name="TransportPathTip3" xml:space="preserve">
|
||||||
<value>*QUIC-ключ / KCP-seed</value>
|
<value>*Ключ шифрования QUIC</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip4" xml:space="preserve">
|
<data name="TransportPathTip4" xml:space="preserve">
|
||||||
<value>Имя сервиса gRPC</value>
|
<value>Имя сервиса gRPC</value>
|
||||||
@@ -349,13 +349,13 @@
|
|||||||
<value>*http-хосты, разделённые запятыми (,)</value>
|
<value>*http-хосты, разделённые запятыми (,)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportRequestHostTip2" xml:space="preserve">
|
<data name="TransportRequestHostTip2" xml:space="preserve">
|
||||||
<value>*Хост ws/http upgrade/xhttp</value>
|
<value>*Хост ws/httpupgrade/xhttp</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportRequestHostTip3" xml:space="preserve">
|
<data name="TransportRequestHostTip3" xml:space="preserve">
|
||||||
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
|
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportRequestHostTip4" xml:space="preserve">
|
<data name="TransportRequestHostTip4" xml:space="preserve">
|
||||||
<value>Безопасность *QUIC</value>
|
<value>*Метод шифрования QUIC</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportHeaderType1" xml:space="preserve">
|
<data name="TransportHeaderType1" xml:space="preserve">
|
||||||
<value>Тип raw-камуфляжа</value>
|
<value>Тип raw-камуфляжа</value>
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
<value>Фильтр, нажмите Enter для выполнения</value>
|
<value>Фильтр, нажмите Enter для выполнения</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckUpdate" xml:space="preserve">
|
<data name="menuCheckUpdate" xml:space="preserve">
|
||||||
<value>Проверить обновления</value>
|
<value>Обновить</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuClose" xml:space="preserve">
|
<data name="menuClose" xml:space="preserve">
|
||||||
<value>Закрыть</value>
|
<value>Закрыть</value>
|
||||||
@@ -415,13 +415,13 @@
|
|||||||
<value>Помощь</value>
|
<value>Помощь</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuOptionSetting" xml:space="preserve">
|
<data name="menuOptionSetting" xml:space="preserve">
|
||||||
<value>Настройки</value>
|
<value>Параметры</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuPromotion" xml:space="preserve">
|
<data name="menuPromotion" xml:space="preserve">
|
||||||
<value>Продвижение</value>
|
<value>Продвижение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuReload" xml:space="preserve">
|
<data name="menuReload" xml:space="preserve">
|
||||||
<value>Перезагрузить</value>
|
<value>Перезапустить службу</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRoutingSetting" xml:space="preserve">
|
<data name="menuRoutingSetting" xml:space="preserve">
|
||||||
<value>Настройки маршрутизации</value>
|
<value>Настройки маршрутизации</value>
|
||||||
@@ -445,10 +445,10 @@
|
|||||||
<value>Настройки групп подписки</value>
|
<value>Настройки групп подписки</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuSubUpdate" xml:space="preserve">
|
<data name="menuSubUpdate" xml:space="preserve">
|
||||||
<value>Обновить подписку без прокси</value>
|
<value>Обновить все подписки без прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuSubUpdateViaProxy" xml:space="preserve">
|
<data name="menuSubUpdateViaProxy" xml:space="preserve">
|
||||||
<value>Обновить подписку с прокси</value>
|
<value>Обновить все подписки через прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuSystemproxy" xml:space="preserve">
|
<data name="menuSystemproxy" xml:space="preserve">
|
||||||
<value>Системный прокси</value>
|
<value>Системный прокси</value>
|
||||||
@@ -562,7 +562,7 @@
|
|||||||
<value>Сортировка</value>
|
<value>Сортировка</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvUserAgent" xml:space="preserve">
|
<data name="LvUserAgent" xml:space="preserve">
|
||||||
<value>User-Agent</value>
|
<value>User-Agent (необязательно)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbCancel" xml:space="preserve">
|
<data name="TbCancel" xml:space="preserve">
|
||||||
<value>Отмена</value>
|
<value>Отмена</value>
|
||||||
@@ -577,7 +577,7 @@
|
|||||||
<value>Адрес</value>
|
<value>Адрес</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecure" xml:space="preserve">
|
<data name="TbAllowInsecure" xml:space="preserve">
|
||||||
<value>Разрешить небезопасные</value>
|
<value>Пропустить проверку сертификата (allowInsecure)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAlpn" xml:space="preserve">
|
<data name="TbAlpn" xml:space="preserve">
|
||||||
<value>ALPN</value>
|
<value>ALPN</value>
|
||||||
@@ -616,10 +616,10 @@
|
|||||||
<value>TLS</value>
|
<value>TLS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TipNetwork" xml:space="preserve">
|
<data name="TipNetwork" xml:space="preserve">
|
||||||
<value>*По-умолчанию raw</value>
|
<value>*По умолчанию raw; при неверном выборе подключение будет невозможно</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbCoreType" xml:space="preserve">
|
<data name="TbCoreType" xml:space="preserve">
|
||||||
<value>Ядро</value>
|
<value>Тип ядра</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFlow5" xml:space="preserve">
|
<data name="TbFlow5" xml:space="preserve">
|
||||||
<value>Управление потоком (Flow)</value>
|
<value>Управление потоком (Flow)</value>
|
||||||
@@ -640,7 +640,7 @@
|
|||||||
<value>Шифрование</value>
|
<value>Шифрование</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSecurity4" xml:space="preserve">
|
<data name="TbSecurity4" xml:space="preserve">
|
||||||
<value>Пользователь (необязательно)</value>
|
<value>Имя пользователя (необязательно)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSecurity5" xml:space="preserve">
|
<data name="TbSecurity5" xml:space="preserve">
|
||||||
<value>Шифрование</value>
|
<value>Шифрование</value>
|
||||||
@@ -649,7 +649,7 @@
|
|||||||
<value>Порт SOCKS</value>
|
<value>Порт SOCKS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TipPreSocksPort" xml:space="preserve">
|
<data name="TipPreSocksPort" xml:space="preserve">
|
||||||
<value>* После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости</value>
|
<value>*Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbBrowse" xml:space="preserve">
|
<data name="TbBrowse" xml:space="preserve">
|
||||||
<value>Обзор</value>
|
<value>Обзор</value>
|
||||||
@@ -682,7 +682,7 @@
|
|||||||
<value>Настройки типа ядра</value>
|
<value>Настройки типа ядра</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsDefAllowInsecure" xml:space="preserve">
|
<data name="TbSettingsDefAllowInsecure" xml:space="preserve">
|
||||||
<value>Разрешить небезопасные</value>
|
<value>По умолчанию пропускать проверку сертификата (allowInsecure)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
|
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
|
||||||
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
|
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
|
||||||
@@ -703,7 +703,7 @@
|
|||||||
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
|
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
|
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
|
||||||
<value>Сохранить старые при удалении дублей</value>
|
<value>При удалении дубликатов сохранять элементы с меньшим порядковым номером</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsLogEnabled" xml:space="preserve">
|
<data name="TbSettingsLogEnabled" xml:space="preserve">
|
||||||
<value>Записывать логи</value>
|
<value>Записывать логи</value>
|
||||||
@@ -730,13 +730,13 @@
|
|||||||
<value>Смешанный порт</value>
|
<value>Смешанный порт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsStartBoot" xml:space="preserve">
|
<data name="TbSettingsStartBoot" xml:space="preserve">
|
||||||
<value>Запускать при старте системы</value>
|
<value>Запускать при старте системы (может не сработать)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsStatistics" xml:space="preserve">
|
<data name="TbSettingsStatistics" xml:space="preserve">
|
||||||
<value>Включить статистику трафика (требуется перезапуск)</value>
|
<value>Включить статистику трафика (требуется перезапуск)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSubConvert" xml:space="preserve">
|
<data name="TbSettingsSubConvert" xml:space="preserve">
|
||||||
<value>URL конвертации подписок</value>
|
<value>URL конвертации подписок (необязательно)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSystemproxy" xml:space="preserve">
|
<data name="TbSettingsSystemproxy" xml:space="preserve">
|
||||||
<value>Настройки системного прокси</value>
|
<value>Настройки системного прокси</value>
|
||||||
@@ -787,13 +787,13 @@
|
|||||||
<value>Запущено от имени администратора</value>
|
<value>Запущено от имени администратора</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveBottom" xml:space="preserve">
|
<data name="menuMoveBottom" xml:space="preserve">
|
||||||
<value>Спуститься вниз</value>
|
<value>Переместить в самый низ</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveDown" xml:space="preserve">
|
<data name="menuMoveDown" xml:space="preserve">
|
||||||
<value>Вниз</value>
|
<value>Вниз</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveTop" xml:space="preserve">
|
<data name="menuMoveTop" xml:space="preserve">
|
||||||
<value>Подняться наверх</value>
|
<value>Переместить в самый верх</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveUp" xml:space="preserve">
|
<data name="menuMoveUp" xml:space="preserve">
|
||||||
<value>Вверх</value>
|
<value>Вверх</value>
|
||||||
@@ -805,7 +805,7 @@
|
|||||||
<value>Веб-сайт {0}</value>
|
<value>Веб-сайт {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
|
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
|
||||||
<value>Добавить</value>
|
<value>Добавить набор правил</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
|
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
|
||||||
<value>Импортировать правила</value>
|
<value>Импортировать правила</value>
|
||||||
@@ -841,7 +841,7 @@
|
|||||||
<value>Добавить правило</value>
|
<value>Добавить правило</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRuleExportSelected" xml:space="preserve">
|
<data name="menuRuleExportSelected" xml:space="preserve">
|
||||||
<value>Экспорт выделенных правил</value>
|
<value>Экспортировать выбранные правила в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRuleList" xml:space="preserve">
|
<data name="menuRuleList" xml:space="preserve">
|
||||||
<value>Список правил</value>
|
<value>Список правил</value>
|
||||||
@@ -859,7 +859,7 @@
|
|||||||
<value>Документация RuleObject</value>
|
<value>Документация RuleObject</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||||
<value>Поддерживаются DNS-объекты, нажмите для просмотра документации</value>
|
<value>Заполните DNS-объект (формат JSON), нажмите для просмотра документации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SubUrlTips" xml:space="preserve">
|
<data name="SubUrlTips" xml:space="preserve">
|
||||||
<value>Для группы оставьте это поле пустым</value>
|
<value>Для группы оставьте это поле пустым</value>
|
||||||
@@ -871,13 +871,13 @@
|
|||||||
<value>Системные прокси изменены</value>
|
<value>Системные прокси изменены</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsRouteOnly" xml:space="preserve">
|
<data name="TbSettingsRouteOnly" xml:space="preserve">
|
||||||
<value>Только маршрут</value>
|
<value>Только для маршрутизации (routeOnly)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
|
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
|
||||||
<value>Не использовать прокси для локальных (интранет) адресов</value>
|
<value>Не использовать прокси для локальных (интранет) адресов</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMixedTestServer" xml:space="preserve">
|
<data name="menuMixedTestServer" xml:space="preserve">
|
||||||
<value>Тест задержки и скорости всех серверов (Ctrl+E)</value>
|
<value>Многопоточный тест задержки и скорости (Ctrl+E)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvTestDelay" xml:space="preserve">
|
<data name="LvTestDelay" xml:space="preserve">
|
||||||
<value>Задержка (мс)</value>
|
<value>Задержка (мс)</value>
|
||||||
@@ -886,10 +886,10 @@
|
|||||||
<value>Скорость (МБ/с)</value>
|
<value>Скорость (МБ/с)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FailedToRunCore" xml:space="preserve">
|
<data name="FailedToRunCore" xml:space="preserve">
|
||||||
<value>Не удалось запустить ядро, посмотрите логи</value>
|
<value>Не удалось запустить ядро, см. сообщение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvFilter" xml:space="preserve">
|
<data name="LvFilter" xml:space="preserve">
|
||||||
<value>Фильтр по примечаниям (регулярные выражения)</value>
|
<value>Фильтр по псевдониму (регулярные выражения)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDisplayLog" xml:space="preserve">
|
<data name="TbDisplayLog" xml:space="preserve">
|
||||||
<value>Отображать журнал</value>
|
<value>Отображать журнал</value>
|
||||||
@@ -937,7 +937,7 @@
|
|||||||
<value>Шрифт (требуется перезапуск)</value>
|
<value>Шрифт (требуется перезапуск)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
|
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
|
||||||
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек</value>
|
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSocksPortTip" xml:space="preserve">
|
<data name="TbSettingsSocksPortTip" xml:space="preserve">
|
||||||
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
|
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
|
||||||
@@ -1009,7 +1009,7 @@
|
|||||||
<value>Пользовательский DNS для sing-box</value>
|
<value>Пользовательский DNS для sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||||
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
|
<value>Заполните структуру DNS (формат JSON), нажмите для просмотра документации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingDnsImportDefConfig" xml:space="preserve">
|
<data name="TbSettingDnsImportDefConfig" xml:space="preserve">
|
||||||
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
|
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
|
||||||
@@ -1033,7 +1033,7 @@
|
|||||||
<value>Добавить сервер [Hysteria2]</value>
|
<value>Добавить сервер [Hysteria2]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
||||||
<value>Максимальная пропускная способность Hysteria (загрузка/отдача)</value>
|
<value>Максимальная пропускная способность Hysteria (отдача/загрузка)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
||||||
<value>Использовать системный файл hosts</value>
|
<value>Использовать системный файл hosts</value>
|
||||||
@@ -1045,13 +1045,13 @@
|
|||||||
<value>Управление перегрузками</value>
|
<value>Управление перегрузками</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvPrevProfile" xml:space="preserve">
|
<data name="LvPrevProfile" xml:space="preserve">
|
||||||
<value>Примечания к предыдущему прокси</value>
|
<value>Псевдоним предыдущего прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvNextProfile" xml:space="preserve">
|
<data name="LvNextProfile" xml:space="preserve">
|
||||||
<value>Примечания к следующему прокси</value>
|
<value>Псевдоним следующего прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||||
<value>Убедитесь, что примечание существует и является уникальным</value>
|
<value>Убедитесь, что псевдоним существует и является уникальным</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||||
<value>Автоматическая маршрутизация</value>
|
<value>Автоматическая маршрутизация</value>
|
||||||
@@ -1104,6 +1104,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Включить фрагментацию (Fragment)</value>
|
<value>Включить фрагментацию (Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Пакеты для фрагментации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Пакеты для фрагментации: tlshello (TLS ClientHello) или 1-1 — 1-5 (первые N TCP-пакетов)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Длина фрагмента</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Интервал фрагментации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Макс. разделение</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Резервная задержка</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1177,7 +1210,7 @@
|
|||||||
<value>Глобальный режим</value>
|
<value>Глобальный режим</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuModeNothing" xml:space="preserve">
|
<data name="menuModeNothing" xml:space="preserve">
|
||||||
<value>Не менять</value>
|
<value>Как в исходной конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuModeRule" xml:space="preserve">
|
<data name="menuModeRule" xml:space="preserve">
|
||||||
<value>Правила</value>
|
<value>Правила</value>
|
||||||
@@ -1210,7 +1243,7 @@
|
|||||||
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
|
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
|
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
|
||||||
<value>Экспортировать полную конфигурацию в буфер обмена</value>
|
<value>Экспортировать выбранную полную конфигурацию в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuShowOrHideMainWindow" xml:space="preserve">
|
<data name="menuShowOrHideMainWindow" xml:space="preserve">
|
||||||
<value>Показать или скрыть главное окно</value>
|
<value>Показать или скрыть главное окно</value>
|
||||||
@@ -1300,7 +1333,7 @@
|
|||||||
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
|
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
|
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
|
||||||
<value>Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки</value>
|
<value>Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuExitTips" xml:space="preserve">
|
<data name="menuExitTips" xml:space="preserve">
|
||||||
<value>Вы уверены, что хотите выйти?</value>
|
<value>Вы уверены, что хотите выйти?</value>
|
||||||
@@ -1318,13 +1351,13 @@
|
|||||||
<value>XHTTP-режим</value>
|
<value>XHTTP-режим</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportExtraTip" xml:space="preserve">
|
<data name="TransportExtraTip" xml:space="preserve">
|
||||||
<value>Сырой JSON, формат: { XHTTP Object }</value>
|
<value>Сырой JSON, формат: { XHTTPObject }</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
|
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
|
||||||
<value>Сворачивать в трей при закрытии окна</value>
|
<value>Сворачивать в трей при закрытии окна</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
|
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
|
||||||
<value>Количество одновременно выполняемых тестов при многоэтапном тестировании</value>
|
<value>Количество параллельных задач при многопоточном тестировании</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsExceptionTip2" xml:space="preserve">
|
<data name="TbSettingsExceptionTip2" xml:space="preserve">
|
||||||
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
|
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
|
||||||
@@ -1336,7 +1369,7 @@
|
|||||||
<value>Включить второй смешанный порт</value>
|
<value>Включить второй смешанный порт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
||||||
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
|
<value>tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsTheme" xml:space="preserve">
|
<data name="TbSettingsTheme" xml:space="preserve">
|
||||||
<value>Тема</value>
|
<value>Тема</value>
|
||||||
@@ -1357,7 +1390,7 @@
|
|||||||
<value>Удалено {0} недействительных</value>
|
<value>Удалено {0} недействительных</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPorts7" xml:space="preserve">
|
<data name="TbPorts7" xml:space="preserve">
|
||||||
<value>Диапазон портов сервера</value>
|
<value>Диапазон портов для переключения (Port Hopping)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPorts7Tips" xml:space="preserve">
|
<data name="TbPorts7Tips" xml:space="preserve">
|
||||||
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
|
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
|
||||||
@@ -1369,7 +1402,7 @@
|
|||||||
<value>URL для тестирования текущего соединения</value>
|
<value>URL для тестирования текущего соединения</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRuleOutboundTagTip" xml:space="preserve">
|
<data name="TbRuleOutboundTagTip" xml:space="preserve">
|
||||||
<value>Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально</value>
|
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
||||||
<value>Неверный пароль, попробуйте ещё раз.</value>
|
<value>Неверный пароль, попробуйте ещё раз.</value>
|
||||||
@@ -1384,7 +1417,7 @@
|
|||||||
<value>Удалённый DNS</value>
|
<value>Удалённый DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDomesticDNS" xml:space="preserve">
|
<data name="TbDomesticDNS" xml:space="preserve">
|
||||||
<value>Внутренний DNS</value>
|
<value>DNS для прямых подключений</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDirectResolveStrategy" xml:space="preserve">
|
<data name="TbDirectResolveStrategy" xml:space="preserve">
|
||||||
<value>Стратегия разрешения прямых соединений</value>
|
<value>Стратегия разрешения прямых соединений</value>
|
||||||
@@ -1489,13 +1522,13 @@
|
|||||||
<value>Тип группы политик</value>
|
<value>Тип группы политик</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||||
<value>Добавить группу политик </value>
|
<value>Добавить группу политик</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||||
<value>Добавить цепочку прокси</value>
|
<value>Добавить цепочку прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuAddChildServer" xml:space="preserve">
|
<data name="menuAddChildServer" xml:space="preserve">
|
||||||
<value>Добавить дочернюю конфигурацию </value>
|
<value>Добавить дочернюю конфигурацию</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||||
<value>Удалить дочернюю конфигурацию </value>
|
<value>Удалить дочернюю конфигурацию </value>
|
||||||
@@ -1534,7 +1567,7 @@
|
|||||||
<value>Bootstrap DNS</value>
|
<value>Bootstrap DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbBootstrapDNSTips" xml:space="preserve">
|
<data name="TbBootstrapDNSTips" xml:space="preserve">
|
||||||
<value>Разрешает домены DNS-серверов, требуется IP-адрес</value>
|
<value>Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuFastRealPing" xml:space="preserve">
|
<data name="menuFastRealPing" xml:space="preserve">
|
||||||
<value>Тест реальной задержки</value>
|
<value>Тест реальной задержки</value>
|
||||||
@@ -1547,7 +1580,7 @@
|
|||||||
</data>
|
</data>
|
||||||
<data name="TbCertPinningTips" xml:space="preserve">
|
<data name="TbCertPinningTips" xml:space="preserve">
|
||||||
<value>Привязанный сертификат (заполните любое из полей)
|
<value>Привязанный сертификат (заполните любое из полей)
|
||||||
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится.
|
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
|
||||||
|
|
||||||
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
|
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1698,12 +1731,6 @@
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>Дополнительные параметры XHTTP (Extra)</value>
|
<value>Дополнительные параметры XHTTP (Extra)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Разрешить небезопасную загрузку сертификата (самоподписанного)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM).</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Тест UDP-задержки конфигураций</value>
|
<value>Тест UDP-задержки конфигураций</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,14 +1743,11 @@
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
|
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Привязать интерфейс</value>
|
<value>Привязать интерфейс</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||||
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
|
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>Общий ключ (PSK)</value>
|
<value>Общий ключ (PSK)</value>
|
||||||
@@ -1732,12 +1756,18 @@
|
|||||||
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
|
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
||||||
<value>{0} has a new version available: {1}</value>
|
<value>Доступна новая версия {0}: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckOnly" xml:space="preserve">
|
<data name="menuCheckOnly" xml:space="preserve">
|
||||||
<value>Only Check</value>
|
<value>Проверить</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Не поддерживается</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>Информация об IP</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>Доступно обновление</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1030,7 +1030,7 @@
|
|||||||
<value>添加 [Hysteria2]</value>
|
<value>添加 [Hysteria2]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
||||||
<value>Hysteria 最大带宽 (Up/Dw)</value>
|
<value>Hysteria 最大带宽 (上传/下载)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
||||||
<value>使用系统 hosts</value>
|
<value>使用系统 hosts</value>
|
||||||
@@ -1101,6 +1101,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>启用分片 (Fragment)</value>
|
<value>启用分片 (Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>分片包类型</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>要分片的包类型: tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 个 TCP 包)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>分片长度</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>分片大小范围(字节), 如 50-100. 第一个值必须 <= 第二个值. 留空使用默认值.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>分片间隔</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>分片间延迟(毫秒), 如 10-20. 范围 1-100. 第一个值必须 <= 第二个值. 留空使用默认值.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>最大分片数</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>每个包最大分片数 (0-10000, 0=无限制). 仅 Xray. 留空使用默认值.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>回退延迟</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>ACK 检测不可用时的回退延迟 (如 500ms). 仅 sing-box. 留空使用默认值.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>无效的范围格式。请使用 'from-to' (如 50-100)。</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>启用 sing-box (规则集文件) 的缓存文件</value>
|
<value>启用 sing-box (规则集文件) 的缓存文件</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1695,12 +1728,6 @@
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>允许不安全获取证书(自签名)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>仅用于抓取自签证书,存在中间人风险。</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>测试 UDP 延迟 (多选)</value>
|
<value>测试 UDP 延迟 (多选)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1713,9 +1740,6 @@
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>用于多网口环境,请填写本机 IPv4 地址</value>
|
<value>用于多网口环境,请填写本机 IPv4 地址</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>请填写正确的 SendThrough IPv4 地址。</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>绑定网口</value>
|
<value>绑定网口</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1737,4 +1761,52 @@
|
|||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>不支持</value>
|
<value>不支持</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>IP 信息</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>有更新</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
|
<value>警告:Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value>
|
||||||
|
</data>
|
||||||
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
|
<value>Verify Peer Cert By Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
|
<value>路由排除地址</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||||
|
<value>使用逗号 (,) 分隔</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||||
|
<value>TUN 路由排除列表包含无效地址:{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgOptionsConflict" xml:space="preserve">
|
||||||
|
<value>{0} 与 {1} 冲突</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragment" xml:space="preserve">
|
||||||
|
<value>启用末端分片</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
|
<value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
|
<value>DNS 通过 Bridge</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||||
|
<value>检测到不安全配置:AllowInsecure 已启用,但未提供证书。这可能会导致中间人攻击。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Realm URL</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Realm URL 不正确。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||||
|
<value>格式:realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||||
|
<value>Gecko 包大小 (最小/最大)</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1101,6 +1101,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>啟用分片(Fragment)</value>
|
<value>啟用分片(Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>分片封包類型</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>要分片的封包類型:tlshello (TLS ClientHello) 或 1-1 到 1-5 (前 N 個 TCP 封包)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>分片長度</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>分片大小範圍(位元組),如 50-100。第一個值必須 <= 第二個值。留空使用預設值。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>分片間隔</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>分片間延遲(毫秒),如 10-20。範圍 1-100。第一個值必須 <= 第二個值。留空使用預設值。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>最大分片數</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>每個封包最大分片數 (0-10000, 0=無限制)。僅 Xray。留空使用預設值。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>回退延遲</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>ACK 偵測不可用時的回退延遲 (如 500ms)。僅 sing-box。留空使用預設值。</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>無效的範圍格式。請使用 'from-to' (如 50-100)。</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>啟用 sing-box(規則集檔案)的快取檔案</value>
|
<value>啟用 sing-box(規則集檔案)的快取檔案</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1333,7 +1366,7 @@
|
|||||||
<value>開啟第二個本機監聽埠</value>
|
<value>開啟第二個本機監聽埠</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
||||||
<value>socks:本地埠,socks2:第二個本地埠,socks3:區域網路埠</value>
|
<value>Tun:TUN 入站,Socks:本機連接埠,Socks2:第二個本機連接埠,Socks3:區域網路連接埠</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsTheme" xml:space="preserve">
|
<data name="TbSettingsTheme" xml:space="preserve">
|
||||||
<value>主題</value>
|
<value>主題</value>
|
||||||
@@ -1695,12 +1728,6 @@
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>允許不安全獲取證書(自簽名)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>僅用於抓取自簽證書,存在中間人風險。</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>測試 UDP 延遲(多選)</value>
|
<value>測試 UDP 延遲(多選)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1713,9 +1740,6 @@
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
|
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>請填寫正確的 SendThrough IPv4 位址。</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>綁定網路介面</value>
|
<value>綁定網路介面</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1737,4 +1761,37 @@
|
|||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>不支援</value>
|
<value>不支援</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
|
<value>IP 資訊</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
|
<value>有更新</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
|
<value>警告:Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
|
<value>Verify Peer Cert By Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
|
<value>路由排除位址</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||||
|
<value>使用逗號 (,) 分隔</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||||
|
<value>TUN 路由排除清單中的位址無效: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgOptionsConflict" xml:space="preserve">
|
||||||
|
<value>{0} 與 {1} 發生衝突</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragment" xml:space="preserve">
|
||||||
|
<value>啟用末端分片</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
|
<value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
|
<value>DNS 透過 Bridge</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -17,14 +17,6 @@
|
|||||||
],
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
"domainStrategy": "IPIfNonMatch",
|
"domainStrategy": "IPIfNonMatch",
|
||||||
"rules": [
|
"rules": []
|
||||||
{
|
|
||||||
"inboundTag": [
|
|
||||||
"api"
|
|
||||||
],
|
|
||||||
"outboundTag": "api",
|
|
||||||
"type": "field"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
"172.18.0.1/30",
|
"172.18.0.1/30",
|
||||||
"fdfe:dcba:9876::1/126"
|
"fdfe:dcba:9876::1/126"
|
||||||
],
|
],
|
||||||
|
"dns": [
|
||||||
|
"172.18.0.1"
|
||||||
|
],
|
||||||
"autoSystemRoutingTable": [
|
"autoSystemRoutingTable": [
|
||||||
"0.0.0.0/0",
|
"0.0.0.0/0",
|
||||||
"::/0"
|
"::/0"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Downloader" />
|
<PackageReference Include="Downloader" />
|
||||||
|
<PackageReference Include="IPNetwork2" />
|
||||||
<PackageReference Include="ReactiveUI">
|
<PackageReference Include="ReactiveUI">
|
||||||
<TreatAsUsed>true</TreatAsUsed>
|
<TreatAsUsed>true</TreatAsUsed>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
</Compile>
|
</Compile>
|
||||||
<EmbeddedResource Update="Resx\ResUI.fa-Ir.resx">
|
<EmbeddedResource Update="Resx\ResUI.fa.resx">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
@@ -66,6 +67,9 @@
|
|||||||
<EmbeddedResource Update="Resx\ResUI.hu.resx">
|
<EmbeddedResource Update="Resx\ResUI.hu.resx">
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Resx\ResUI.id.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resx\ResUI.resx">
|
<EmbeddedResource Update="Resx\ResUI.resx">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<LastGenOutput>ResUI.Designer.cs</LastGenOutput>
|
<LastGenOutput>ResUI.Designer.cs</LastGenOutput>
|
||||||
|
|||||||
@@ -3,16 +3,10 @@ namespace ServiceLib.Services.CoreConfig;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core configuration file processing class
|
/// Core configuration file processing class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CoreConfigClashService
|
public class CoreConfigClashService(Config config)
|
||||||
{
|
{
|
||||||
private Config _config;
|
|
||||||
private static readonly string _tag = "CoreConfigClashService";
|
private static readonly string _tag = "CoreConfigClashService";
|
||||||
|
|
||||||
public CoreConfigClashService(Config config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
|
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
|
||||||
{
|
{
|
||||||
var ret = new RetResult();
|
var ret = new RetResult();
|
||||||
@@ -56,7 +50,7 @@ public class CoreConfigClashService
|
|||||||
var tagYamlStr1 = "!<str>";
|
var tagYamlStr1 = "!<str>";
|
||||||
var tagYamlStr2 = "__strn__";
|
var tagYamlStr2 = "__strn__";
|
||||||
var tagYamlStr3 = "!!str";
|
var tagYamlStr3 = "!!str";
|
||||||
var txtFile = File.ReadAllText(addressFileName);
|
var txtFile = await File.ReadAllTextAsync(addressFileName);
|
||||||
txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2);
|
txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2);
|
||||||
|
|
||||||
//YAML anchors
|
//YAML anchors
|
||||||
@@ -75,13 +69,13 @@ public class CoreConfigClashService
|
|||||||
//mixed-port
|
//mixed-port
|
||||||
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||||
//log-level
|
//log-level
|
||||||
fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel);
|
fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
|
||||||
|
|
||||||
//external-controller
|
//external-controller
|
||||||
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
|
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
|
||||||
fileContent.Remove("secret");
|
fileContent.Remove("secret");
|
||||||
//allow-lan
|
//allow-lan
|
||||||
if (_config.Inbound.First().AllowLANConn)
|
if (config.Inbound.First().AllowLANConn)
|
||||||
{
|
{
|
||||||
fileContent["allow-lan"] = "true";
|
fileContent["allow-lan"] = "true";
|
||||||
fileContent["bind-address"] = "*";
|
fileContent["bind-address"] = "*";
|
||||||
@@ -92,23 +86,23 @@ public class CoreConfigClashService
|
|||||||
}
|
}
|
||||||
|
|
||||||
//ipv6
|
//ipv6
|
||||||
fileContent["ipv6"] = _config.ClashUIItem.EnableIPv6;
|
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
|
||||||
|
|
||||||
//mode
|
//mode
|
||||||
if (!fileContent.ContainsKey("mode"))
|
if (!fileContent.ContainsKey("mode"))
|
||||||
{
|
{
|
||||||
fileContent["mode"] = ERuleMode.Rule.ToString().ToLower();
|
fileContent["mode"] = nameof(ERuleMode.Rule).ToLower();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
|
if (config.ClashUIItem.RuleMode != ERuleMode.Unchanged)
|
||||||
{
|
{
|
||||||
fileContent["mode"] = _config.ClashUIItem.RuleMode.ToString().ToLower();
|
fileContent["mode"] = config.ClashUIItem.RuleMode.ToString().ToLower();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//enable tun mode
|
//enable tun mode
|
||||||
if (_config.TunModeItem.EnableTun)
|
if (config.TunModeItem.EnableTun)
|
||||||
{
|
{
|
||||||
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
|
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
|
||||||
if (tun.IsNotEmpty())
|
if (tun.IsNotEmpty())
|
||||||
@@ -156,7 +150,7 @@ public class CoreConfigClashService
|
|||||||
|
|
||||||
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
|
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
|
||||||
{
|
{
|
||||||
if (!_config.ClashUIItem.EnableMixinContent)
|
if (!config.ClashUIItem.EnableMixinContent)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -177,7 +171,7 @@ public class CoreConfigClashService
|
|||||||
}
|
}
|
||||||
foreach (var item in mixinContent)
|
foreach (var item in mixinContent)
|
||||||
{
|
{
|
||||||
if (!_config.TunModeItem.EnableTun && item.Key == "tun")
|
if (!config.TunModeItem.EnableTun && item.Key == "tun")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -220,9 +214,8 @@ public class CoreConfigClashService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!blRemoved && !fileContent.ContainsKey(key))
|
if (!blRemoved && fileContent.TryAdd(key, value))
|
||||||
{
|
{
|
||||||
fileContent.Add(key, value);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var lstOri = (List<object>)fileContent[key];
|
var lstOri = (List<object>)fileContent[key];
|
||||||
@@ -244,7 +237,7 @@ public class CoreConfigClashService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lstValue.ForEach(item => lstOri.Add(item));
|
lstValue.ForEach(lstOri.Add);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
|||||||
{
|
{
|
||||||
listen = Global.Loopback,
|
listen = Global.Loopback,
|
||||||
listen_port = port,
|
listen_port = port,
|
||||||
type = EInboundProtocol.mixed.ToString(),
|
type = nameof(EInboundProtocol.mixed),
|
||||||
};
|
};
|
||||||
inbound.tag = inbound.type + inbound.listen_port.ToString();
|
inbound.tag = inbound.type + inbound.listen_port.ToString();
|
||||||
_coreConfig.inbounds.Add(inbound);
|
_coreConfig.inbounds.Add(inbound);
|
||||||
@@ -229,7 +229,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
|||||||
tag = $"{EInboundProtocol.mixed}{port}",
|
tag = $"{EInboundProtocol.mixed}{port}",
|
||||||
listen = Global.Loopback,
|
listen = Global.Loopback,
|
||||||
listen_port = port,
|
listen_port = port,
|
||||||
type = EInboundProtocol.mixed.ToString(),
|
type = nameof(EInboundProtocol.mixed),
|
||||||
});
|
});
|
||||||
ApplyOutboundBindInterface();
|
ApplyOutboundBindInterface();
|
||||||
ApplyOutboundSendThrough();
|
ApplyOutboundSendThrough();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public partial class CoreConfigSingboxService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process outbounds
|
// Process outbounds
|
||||||
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : [];
|
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
|
||||||
foreach (var outbound in _coreConfig.outbounds)
|
foreach (var outbound in _coreConfig.outbounds)
|
||||||
{
|
{
|
||||||
if (outbound.type.ToLower() is "direct" or "block")
|
if (outbound.type.ToLower() is "direct" or "block")
|
||||||
@@ -42,9 +42,9 @@ public partial class CoreConfigSingboxService
|
|||||||
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
||||||
|
|
||||||
// Process endpoints
|
// Process endpoints
|
||||||
if (_coreConfig.endpoints != null && _coreConfig.endpoints.Count > 0)
|
if (_coreConfig.endpoints is { Count: > 0 })
|
||||||
{
|
{
|
||||||
var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : [];
|
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
|
||||||
foreach (var endpoint in _coreConfig.endpoints)
|
foreach (var endpoint in _coreConfig.endpoints)
|
||||||
{
|
{
|
||||||
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
||||||
@@ -66,10 +66,6 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(context.IsTunEnabled || context.IsWindows))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach (var outbound in _coreConfig.outbounds ?? [])
|
foreach (var outbound in _coreConfig.outbounds ?? [])
|
||||||
{
|
{
|
||||||
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
|
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
|
||||||
@@ -83,6 +79,7 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var outbound in _coreConfig.outbounds ?? [])
|
foreach (var outbound in _coreConfig.outbounds ?? [])
|
||||||
{
|
{
|
||||||
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
|
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
||||||
|
|
||||||
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
|
if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule)
|
||||||
{
|
{
|
||||||
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
|
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
|
||||||
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
|
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
|
||||||
@@ -82,7 +82,7 @@ public partial class CoreConfigSingboxService
|
|||||||
if (simpleDnsItem.UseSystemHosts == true)
|
if (simpleDnsItem.UseSystemHosts == true)
|
||||||
{
|
{
|
||||||
var systemHosts = Utils.GetSystemHosts();
|
var systemHosts = Utils.GetSystemHosts();
|
||||||
if (systemHosts != null && systemHosts.Count > 0)
|
if (systemHosts is { Count: > 0 })
|
||||||
{
|
{
|
||||||
foreach (var host in systemHosts)
|
foreach (var host in systemHosts)
|
||||||
{
|
{
|
||||||
@@ -182,13 +182,13 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
server = Global.SingboxRemoteDNSTag,
|
server = Global.SingboxRemoteDNSTag,
|
||||||
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
|
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
|
||||||
clash_mode = ERuleMode.Global.ToString()
|
clash_mode = nameof(ERuleMode.Global)
|
||||||
},
|
},
|
||||||
new Rule4Sbox
|
new Rule4Sbox
|
||||||
{
|
{
|
||||||
server = Global.SingboxDirectDNSTag,
|
server = Global.SingboxDirectDNSTag,
|
||||||
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
|
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
|
||||||
clash_mode = ERuleMode.Direct.ToString()
|
clash_mode = nameof(ERuleMode.Direct)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -454,12 +454,12 @@ public partial class CoreConfigSingboxService
|
|||||||
dns4Sbox.rules.Insert(0, new()
|
dns4Sbox.rules.Insert(0, new()
|
||||||
{
|
{
|
||||||
server = tag,
|
server = tag,
|
||||||
clash_mode = ERuleMode.Direct.ToString()
|
clash_mode = nameof(ERuleMode.Direct)
|
||||||
});
|
});
|
||||||
dns4Sbox.rules.Insert(0, new()
|
dns4Sbox.rules.Insert(0, new()
|
||||||
{
|
{
|
||||||
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
|
server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote",
|
||||||
clash_mode = ERuleMode.Global.ToString()
|
clash_mode = nameof(ERuleMode.Global)
|
||||||
});
|
});
|
||||||
|
|
||||||
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
|
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
var inbound = new Inbound4Sbox()
|
var inbound = new Inbound4Sbox()
|
||||||
{
|
{
|
||||||
type = EInboundProtocol.mixed.ToString(),
|
type = nameof(EInboundProtocol.mixed),
|
||||||
tag = EInboundProtocol.socks.ToString(),
|
tag = nameof(EInboundProtocol.socks),
|
||||||
listen = Global.Loopback,
|
listen = Global.Loopback,
|
||||||
};
|
};
|
||||||
_coreConfig.inbounds.Add(inbound);
|
_coreConfig.inbounds.Add(inbound);
|
||||||
@@ -71,6 +71,7 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
tunInbound.address = ["172.18.0.1/30"];
|
tunInbound.address = ["172.18.0.1/30"];
|
||||||
}
|
}
|
||||||
|
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;
|
||||||
|
|
||||||
_coreConfig.inbounds.Add(tunInbound);
|
_coreConfig.inbounds.Add(tunInbound);
|
||||||
}
|
}
|
||||||
@@ -86,7 +87,7 @@ public partial class CoreConfigSingboxService
|
|||||||
var inbound = JsonUtils.DeepCopy(inItem);
|
var inbound = JsonUtils.DeepCopy(inItem);
|
||||||
inbound.tag = protocol.ToString();
|
inbound.tag = protocol.ToString();
|
||||||
inbound.listen_port = inItem.listen_port + (int)protocol;
|
inbound.listen_port = inItem.listen_port + (int)protocol;
|
||||||
inbound.type = EInboundProtocol.mixed.ToString();
|
inbound.type = nameof(EInboundProtocol.mixed);
|
||||||
return inbound;
|
return inbound;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,15 +190,16 @@ public partial class CoreConfigSingboxService
|
|||||||
|
|
||||||
outbound.packet_encoding = "xudp";
|
outbound.packet_encoding = "xudp";
|
||||||
|
|
||||||
if (!protocolExtra.Flow.IsNullOrEmpty())
|
if (protocolExtra.Flow is "xtls-rprx-vision" or "xtls-rprx-vision-udp443")
|
||||||
|
{
|
||||||
|
outbound.flow = "xtls-rprx-vision";
|
||||||
|
}
|
||||||
|
else if (!protocolExtra.Flow.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
outbound.flow = protocolExtra.Flow;
|
outbound.flow = protocolExtra.Flow;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
FillOutboundMux(outbound);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
FillOutboundMux(outbound);
|
||||||
FillOutboundTransport(outbound);
|
FillOutboundTransport(outbound);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -216,11 +217,17 @@ public partial class CoreConfigSingboxService
|
|||||||
|
|
||||||
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
|
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
outbound.obfs = new()
|
outbound.obfs = new()
|
||||||
{
|
{
|
||||||
type = "salamander",
|
type = isGecko ? "gecko" : "salamander",
|
||||||
password = protocolExtra.SalamanderPass.TrimEx(),
|
password = protocolExtra.SalamanderPass.TrimEx(),
|
||||||
};
|
};
|
||||||
|
if (isGecko)
|
||||||
|
{
|
||||||
|
outbound.obfs.min_packet_size = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||||
|
outbound.obfs.max_packet_size = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
|
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
|
||||||
? su
|
? su
|
||||||
@@ -263,6 +270,22 @@ public partial class CoreConfigSingboxService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
|
||||||
|
&& realm is not null)
|
||||||
|
{
|
||||||
|
var realm4Sbox = new HyRealm4Sbox()
|
||||||
|
{
|
||||||
|
server_url = realm.ToServerUrl(),
|
||||||
|
token = realm.Token,
|
||||||
|
realm_id = realm.RealmName,
|
||||||
|
stun_servers = realm.StunList?.Count > 0 ? realm.StunList : null,
|
||||||
|
};
|
||||||
|
outbound.realm = realm4Sbox;
|
||||||
|
outbound.server = null;
|
||||||
|
outbound.server_port = null;
|
||||||
|
outbound.server_ports = null;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.TUIC:
|
case EConfigType.TUIC:
|
||||||
@@ -342,7 +365,7 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
|
var muxEnabled = _node.MuxEnabled ?? false;
|
||||||
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
|
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
|
||||||
{
|
{
|
||||||
var mux = new Multiplex4Sbox()
|
var mux = new Multiplex4Sbox()
|
||||||
@@ -396,7 +419,7 @@ public partial class CoreConfigSingboxService
|
|||||||
enabled = true,
|
enabled = true,
|
||||||
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
|
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
|
||||||
server_name = serverName,
|
server_name = serverName,
|
||||||
insecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
|
insecure = _node.GetAllowInsecure(),
|
||||||
alpn = _node.GetAlpn(),
|
alpn = _node.GetAlpn(),
|
||||||
};
|
};
|
||||||
if (_node.Fingerprint.IsNotEmpty())
|
if (_node.Fingerprint.IsNotEmpty())
|
||||||
@@ -729,8 +752,8 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
|
var outbounds = servers.OfType<Outbound4Sbox>().ToList();
|
||||||
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
|
var endpoints = servers.OfType<Endpoints4Sbox>().ToList();
|
||||||
singboxConfig.endpoints ??= [];
|
singboxConfig.endpoints ??= [];
|
||||||
if (prepend)
|
if (prepend)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ public partial class CoreConfigSingboxService
|
|||||||
new() { protocol = ["dns"] },
|
new() { protocol = ["dns"] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
if (_config.CoreBasicItem.EnableFinalFragment)
|
||||||
|
{
|
||||||
|
_coreConfig.route.rules.Add(new()
|
||||||
|
{
|
||||||
|
protocol = ["tls"],
|
||||||
|
action = "route-options",
|
||||||
|
tls_record_fragment = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -104,6 +113,14 @@ public partial class CoreConfigSingboxService
|
|||||||
port = [53],
|
port = [53],
|
||||||
action = "hijack-dns",
|
action = "hijack-dns",
|
||||||
});
|
});
|
||||||
|
if (_config.CoreBasicItem.EnableFinalFragment)
|
||||||
|
{
|
||||||
|
_coreConfig.route.rules.Add(new()
|
||||||
|
{
|
||||||
|
action = "route-options",
|
||||||
|
tls_record_fragment = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostsDomains = new List<string>();
|
var hostsDomains = new List<string>();
|
||||||
@@ -176,12 +193,12 @@ public partial class CoreConfigSingboxService
|
|||||||
_coreConfig.route.rules.Add(new()
|
_coreConfig.route.rules.Add(new()
|
||||||
{
|
{
|
||||||
outbound = Global.DirectTag,
|
outbound = Global.DirectTag,
|
||||||
clash_mode = ERuleMode.Direct.ToString()
|
clash_mode = nameof(ERuleMode.Direct)
|
||||||
});
|
});
|
||||||
_coreConfig.route.rules.Add(new()
|
_coreConfig.route.rules.Add(new()
|
||||||
{
|
{
|
||||||
outbound = Global.ProxyTag,
|
outbound = Global.ProxyTag,
|
||||||
clash_mode = ERuleMode.Global.ToString()
|
clash_mode = nameof(ERuleMode.Global)
|
||||||
});
|
});
|
||||||
|
|
||||||
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();
|
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ public partial class CoreConfigSingboxService
|
|||||||
//convert route geosite & geoip to ruleset
|
//convert route geosite & geoip to ruleset
|
||||||
foreach (var rule in _coreConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
|
foreach (var rule in _coreConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
|
||||||
{
|
{
|
||||||
rule.rule_set ??= new List<string>();
|
rule.rule_set ??= [];
|
||||||
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
|
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
|
||||||
rule.geosite = null;
|
rule.geosite = null;
|
||||||
AddRuleSets(ruleSets, rule.rule_set);
|
AddRuleSets(ruleSets, rule.rule_set);
|
||||||
}
|
}
|
||||||
foreach (var rule in _coreConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
|
foreach (var rule in _coreConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
|
||||||
{
|
{
|
||||||
rule.rule_set ??= new List<string>();
|
rule.rule_set ??= [];
|
||||||
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
|
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
|
||||||
rule.geoip = null;
|
rule.geoip = null;
|
||||||
AddRuleSets(ruleSets, rule.rule_set);
|
AddRuleSets(ruleSets, rule.rule_set);
|
||||||
}
|
}
|
||||||
@@ -34,14 +34,14 @@ public partial class CoreConfigSingboxService
|
|||||||
//convert dns geosite & geoip to ruleset
|
//convert dns geosite & geoip to ruleset
|
||||||
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
|
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? [])
|
||||||
{
|
{
|
||||||
rule.rule_set ??= new List<string>();
|
rule.rule_set ??= [];
|
||||||
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList());
|
rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList() ?? []);
|
||||||
rule.geosite = null;
|
rule.geosite = null;
|
||||||
}
|
}
|
||||||
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
|
foreach (var rule in _coreConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? [])
|
||||||
{
|
{
|
||||||
rule.rule_set ??= new List<string>();
|
rule.rule_set ??= [];
|
||||||
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
|
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList() ?? []);
|
||||||
rule.geoip = null;
|
rule.geoip = null;
|
||||||
}
|
}
|
||||||
foreach (var dnsRule in _coreConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])
|
foreach (var dnsRule in _coreConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? [])
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
|
|||||||
{
|
{
|
||||||
ApplyOutboundFragment();
|
ApplyOutboundFragment();
|
||||||
}
|
}
|
||||||
|
if (_config.CoreBasicItem.EnableFinalFragment)
|
||||||
|
{
|
||||||
|
ApplyFinalFragment();
|
||||||
|
}
|
||||||
ApplyOutboundBindInterface();
|
ApplyOutboundBindInterface();
|
||||||
ApplyOutboundSendThrough();
|
ApplyOutboundSendThrough();
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
|
|||||||
{
|
{
|
||||||
listen = Global.Loopback,
|
listen = Global.Loopback,
|
||||||
port = port,
|
port = port,
|
||||||
protocol = EInboundProtocol.mixed.ToString(),
|
protocol = nameof(EInboundProtocol.mixed),
|
||||||
settings = new Inboundsettings4Ray()
|
settings = new Inboundsettings4Ray()
|
||||||
{
|
{
|
||||||
udp = true,
|
udp = true,
|
||||||
@@ -204,6 +208,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
|
|||||||
{
|
{
|
||||||
ApplyOutboundFragment();
|
ApplyOutboundFragment();
|
||||||
}
|
}
|
||||||
|
if (_config.CoreBasicItem.EnableFinalFragment)
|
||||||
|
{
|
||||||
|
ApplyFinalFragment();
|
||||||
|
}
|
||||||
ApplyOutboundBindInterface();
|
ApplyOutboundBindInterface();
|
||||||
ApplyOutboundSendThrough();
|
ApplyOutboundSendThrough();
|
||||||
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
|
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
|
||||||
@@ -262,7 +270,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
|
|||||||
tag = $"{EInboundProtocol.socks}{port}",
|
tag = $"{EInboundProtocol.socks}{port}",
|
||||||
listen = Global.Loopback,
|
listen = Global.Loopback,
|
||||||
port = port,
|
port = port,
|
||||||
protocol = EInboundProtocol.mixed.ToString(),
|
protocol = nameof(EInboundProtocol.mixed),
|
||||||
settings = new Inboundsettings4Ray()
|
settings = new Inboundsettings4Ray()
|
||||||
{
|
{
|
||||||
udp = true,
|
udp = true,
|
||||||
@@ -276,6 +284,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
|
|||||||
{
|
{
|
||||||
ApplyOutboundFragment();
|
ApplyOutboundFragment();
|
||||||
}
|
}
|
||||||
|
if (_config.CoreBasicItem.EnableFinalFragment)
|
||||||
|
{
|
||||||
|
ApplyFinalFragment();
|
||||||
|
}
|
||||||
ApplyOutboundBindInterface();
|
ApplyOutboundBindInterface();
|
||||||
ApplyOutboundSendThrough();
|
ApplyOutboundSendThrough();
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public partial class CoreConfigV2rayService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_coreConfig.burstObservatory.subjectSelector ??= new();
|
_coreConfig.burstObservatory.subjectSelector ??= [];
|
||||||
_coreConfig.burstObservatory.subjectSelector.Add(baseTagName);
|
_coreConfig.burstObservatory.subjectSelector.Add(baseTagName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ public partial class CoreConfigV2rayService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_coreConfig.observatory.subjectSelector ??= new();
|
_coreConfig.observatory.subjectSelector ??= [];
|
||||||
_coreConfig.observatory.subjectSelector.Add(baseTagName);
|
_coreConfig.observatory.subjectSelector.Add(baseTagName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,23 +89,27 @@ public partial class CoreConfigV2rayService
|
|||||||
EMultipleLoad.RoundRobin => "roundRobin",
|
EMultipleLoad.RoundRobin => "roundRobin",
|
||||||
EMultipleLoad.LeastPing => "leastPing",
|
EMultipleLoad.LeastPing => "leastPing",
|
||||||
EMultipleLoad.LeastLoad => "leastLoad",
|
EMultipleLoad.LeastLoad => "leastLoad",
|
||||||
|
EMultipleLoad.Fallback => "leastLoad",
|
||||||
_ => "roundRobin",
|
_ => "roundRobin",
|
||||||
};
|
};
|
||||||
var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
|
var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
|
||||||
var balancer = new BalancersItem4Ray
|
var balancer = new BalancersItem4Ray
|
||||||
{
|
{
|
||||||
selector = [selector],
|
selector = [selector],
|
||||||
strategy = new()
|
strategy = strategyType == "leastLoad" ? new()
|
||||||
{
|
{
|
||||||
type = strategyType,
|
type = strategyType,
|
||||||
settings = new()
|
settings = new()
|
||||||
{
|
{
|
||||||
expected = 1,
|
expected = 1,
|
||||||
|
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
|
||||||
|
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
|
||||||
},
|
},
|
||||||
},
|
} : null,
|
||||||
tag = balancerTag,
|
tag = balancerTag,
|
||||||
|
fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
|
||||||
};
|
};
|
||||||
_coreConfig.routing.balancers ??= new();
|
_coreConfig.routing.balancers ??= [];
|
||||||
_coreConfig.routing.balancers.Add(balancer);
|
_coreConfig.routing.balancers.Add(balancer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,7 @@ public partial class CoreConfigV2rayService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure routing node exists
|
// Ensure routing node exists
|
||||||
if (fullConfigTemplateNode["routing"] == null)
|
fullConfigTemplateNode["routing"] ??= new JsonObject();
|
||||||
{
|
|
||||||
fullConfigTemplateNode["routing"] = new JsonObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle balancers - append instead of override
|
// Handle balancers - append instead of override
|
||||||
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)
|
if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode)
|
||||||
@@ -141,10 +138,6 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(context.IsTunEnabled || context.IsWindows))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach (var outbound in _coreConfig.outbounds ?? [])
|
foreach (var outbound in _coreConfig.outbounds ?? [])
|
||||||
{
|
{
|
||||||
if (!ShouldBindNet(outbound))
|
if (!ShouldBindNet(outbound))
|
||||||
@@ -179,6 +172,7 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var outbound in _coreConfig.outbounds ?? [])
|
foreach (var outbound in _coreConfig.outbounds ?? [])
|
||||||
{
|
{
|
||||||
outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null;
|
outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public partial class CoreConfigV2rayService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var simpleDnsItem = context.SimpleDnsItem;
|
var simpleDnsItem = context.SimpleDnsItem;
|
||||||
var dnsItem = _coreConfig.dns is Dns4Ray dns4Ray ? dns4Ray : new Dns4Ray();
|
var dnsItem = _coreConfig.dns as Dns4Ray ?? new Dns4Ray();
|
||||||
|
|
||||||
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
|
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
|
||||||
//Outbound Freedom domainStrategy
|
//Outbound Freedom domainStrategy
|
||||||
@@ -246,7 +246,7 @@ public partial class CoreConfigV2rayService
|
|||||||
|
|
||||||
var useDirectDns = false;
|
var useDirectDns = false;
|
||||||
|
|
||||||
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
|
if (rules?.LastOrDefault() is { OutboundTag: Global.DirectTag } lastRule)
|
||||||
{
|
{
|
||||||
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
|
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
|
||||||
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
|
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
|
||||||
@@ -384,9 +384,11 @@ public partial class CoreConfigV2rayService
|
|||||||
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
|
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
|
||||||
if (outbound != null)
|
if (outbound != null)
|
||||||
{
|
{
|
||||||
outbound.settings = new();
|
outbound.settings = new()
|
||||||
outbound.settings.domainStrategy = domainStrategy4Freedom;
|
{
|
||||||
outbound.settings.userLevel = 0;
|
domainStrategy = domainStrategy4Freedom,
|
||||||
|
userLevel = 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,19 @@ public partial class CoreConfigV2rayService
|
|||||||
inbound3.listen = listen;
|
inbound3.listen = listen;
|
||||||
_coreConfig.inbounds.Add(inbound3);
|
_coreConfig.inbounds.Add(inbound3);
|
||||||
|
|
||||||
//auth
|
// auth
|
||||||
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
|
if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty())
|
||||||
{
|
{
|
||||||
inbound3.settings.auth = "password";
|
inbound3.settings.auth = "password";
|
||||||
inbound3.settings.accounts = new List<AccountsItem4Ray>
|
inbound3.settings.accounts =
|
||||||
|
[
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass }
|
user = _config.Inbound.First().User,
|
||||||
};
|
pass = _config.Inbound.First().Pass,
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -53,12 +58,15 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
_config.TunModeItem.Mtu = Global.TunMtus.First();
|
_config.TunModeItem.Mtu = Global.TunMtus.First();
|
||||||
}
|
}
|
||||||
var tunInbound = JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { };
|
var tunInbound =
|
||||||
|
JsonUtils.Deserialize<Inbounds4Ray>(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ??
|
||||||
|
new Inbounds4Ray();
|
||||||
tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
|
tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
|
||||||
tunInbound.settings.MTU = _config.TunModeItem.Mtu;
|
tunInbound.settings.MTU = _config.TunModeItem.Mtu;
|
||||||
if (_config.TunModeItem.EnableIPv6Address == false)
|
if (!_config.TunModeItem.EnableIPv6Address)
|
||||||
{
|
{
|
||||||
tunInbound.settings.gateway = ["172.18.0.1/30"];
|
tunInbound.settings.gateway = ["172.18.0.1/30"];
|
||||||
|
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
|
||||||
}
|
}
|
||||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
||||||
if (!bindInterface.IsNullOrEmpty())
|
if (!bindInterface.IsNullOrEmpty())
|
||||||
@@ -66,6 +74,53 @@ public partial class CoreConfigV2rayService
|
|||||||
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
||||||
}
|
}
|
||||||
tunInbound.sniffing = inbound.sniffing;
|
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<IPNetwork2> { wholeInternet };
|
||||||
|
var includeListV6 = new List<IPNetwork2> { wholeInternetV6 };
|
||||||
|
|
||||||
|
foreach (var exclude in excludeList)
|
||||||
|
{
|
||||||
|
var temp = new List<IPNetwork2>();
|
||||||
|
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);
|
_coreConfig.inbounds.Add(tunInbound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +145,7 @@ public partial class CoreConfigV2rayService
|
|||||||
}
|
}
|
||||||
inbound.tag = protocol.ToString();
|
inbound.tag = protocol.ToString();
|
||||||
inbound.port = inItem.LocalPort + (int)protocol;
|
inbound.port = inItem.LocalPort + (int)protocol;
|
||||||
inbound.protocol = EInboundProtocol.mixed.ToString();
|
inbound.protocol = nameof(EInboundProtocol.mixed);
|
||||||
inbound.settings.udp = inItem.UdpEnabled;
|
inbound.settings.udp = inItem.UdpEnabled;
|
||||||
inbound.sniffing.enabled = inItem.SniffingEnabled;
|
inbound.sniffing.enabled = inItem.SniffingEnabled;
|
||||||
inbound.sniffing.destOverride = inItem.DestOverride;
|
inbound.sniffing.destOverride = inItem.DestOverride;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public partial class CoreConfigV2rayService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var protocolExtra = _node.GetProtocolExtra();
|
var protocolExtra = _node.GetProtocolExtra();
|
||||||
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
|
var muxEnabled = _node.MuxEnabled ?? false;
|
||||||
switch (_node.ConfigType)
|
switch (_node.ConfigType)
|
||||||
{
|
{
|
||||||
case EConfigType.VMess:
|
case EConfigType.VMess:
|
||||||
@@ -243,9 +243,9 @@ public partial class CoreConfigV2rayService
|
|||||||
version = 2,
|
version = 2,
|
||||||
address = _node.Address,
|
address = _node.Address,
|
||||||
port = _node.Port,
|
port = _node.Port,
|
||||||
|
vnext = null,
|
||||||
|
servers = null,
|
||||||
};
|
};
|
||||||
outbound.settings.vnext = null;
|
|
||||||
outbound.settings.servers = null;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EConfigType.WireGuard:
|
case EConfigType.WireGuard:
|
||||||
@@ -380,10 +380,11 @@ public partial class CoreConfigV2rayService
|
|||||||
|
|
||||||
TlsSettings4Ray tlsSettings = new()
|
TlsSettings4Ray tlsSettings = new()
|
||||||
{
|
{
|
||||||
allowInsecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
|
allowInsecure = _node.GetAllowInsecure(),
|
||||||
alpn = _node.GetAlpn(),
|
alpn = _node.GetAlpn(),
|
||||||
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
|
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
|
||||||
echConfigList = _node.EchConfigList.NullIfEmpty(),
|
echConfigList = _node.EchConfigList.NullIfEmpty(),
|
||||||
|
verifyPeerCertByName = _node.VerifyPeerCertByName.NullIfEmpty(),
|
||||||
};
|
};
|
||||||
if (sni.IsNotEmpty())
|
if (sni.IsNotEmpty())
|
||||||
{
|
{
|
||||||
@@ -449,14 +450,13 @@ public partial class CoreConfigV2rayService
|
|||||||
KcpSettings4Ray kcpSettings = new()
|
KcpSettings4Ray kcpSettings = new()
|
||||||
{
|
{
|
||||||
mtu = kcpMtu,
|
mtu = kcpMtu,
|
||||||
tti = _config.KcpItem.Tti
|
tti = _config.KcpItem.Tti,
|
||||||
|
uplinkCapacity = _config.KcpItem.UplinkCapacity,
|
||||||
|
downlinkCapacity = _config.KcpItem.DownlinkCapacity,
|
||||||
|
cwndMultiplier = _config.KcpItem.CwndMultiplier,
|
||||||
|
maxSendingWindow = _config.KcpItem.MaxSendingWindow,
|
||||||
};
|
};
|
||||||
|
|
||||||
kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity;
|
|
||||||
kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity;
|
|
||||||
|
|
||||||
kcpSettings.cwndMultiplier = _config.KcpItem.CwndMultiplier;
|
|
||||||
kcpSettings.maxSendingWindow = _config.KcpItem.MaxSendingWindow;
|
|
||||||
var kcpFinalmask = new Finalmask4Ray();
|
var kcpFinalmask = new Finalmask4Ray();
|
||||||
if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header))
|
if (Global.KcpHeaderMaskMap.TryGetValue(headerType, out var header))
|
||||||
{
|
{
|
||||||
@@ -464,8 +464,8 @@ public partial class CoreConfigV2rayService
|
|||||||
[
|
[
|
||||||
new Mask4Ray
|
new Mask4Ray
|
||||||
{
|
{
|
||||||
type = header,
|
type = "mkcp-legacy",
|
||||||
settings = null
|
settings = new MaskSettings4Ray { header = header },
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -474,15 +474,15 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
kcpFinalmask.udp.Add(new Mask4Ray
|
kcpFinalmask.udp.Add(new Mask4Ray
|
||||||
{
|
{
|
||||||
type = "mkcp-original"
|
type = "mkcp-legacy",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
kcpFinalmask.udp.Add(new Mask4Ray
|
kcpFinalmask.udp.Add(new Mask4Ray
|
||||||
{
|
{
|
||||||
type = "mkcp-aes128gcm",
|
type = "mkcp-legacy",
|
||||||
settings = new MaskSettings4Ray { password = kcpSeed }
|
settings = new MaskSettings4Ray { value = kcpSeed },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
streamSettings.kcpSettings = kcpSettings;
|
streamSettings.kcpSettings = kcpSettings;
|
||||||
@@ -530,7 +530,7 @@ public partial class CoreConfigV2rayService
|
|||||||
break;
|
break;
|
||||||
//xhttp
|
//xhttp
|
||||||
case nameof(ETransport.xhttp):
|
case nameof(ETransport.xhttp):
|
||||||
streamSettings.network = ETransport.xhttp.ToString();
|
streamSettings.network = nameof(ETransport.xhttp);
|
||||||
XhttpSettings4Ray xhttpSettings = new();
|
XhttpSettings4Ray xhttpSettings = new();
|
||||||
|
|
||||||
if (path.IsNotEmpty())
|
if (path.IsNotEmpty())
|
||||||
@@ -607,16 +607,32 @@ public partial class CoreConfigV2rayService
|
|||||||
quicParams.congestion = "bbr";
|
quicParams.congestion = "bbr";
|
||||||
}
|
}
|
||||||
hy2Finalmask.quicParams = quicParams;
|
hy2Finalmask.quicParams = quicParams;
|
||||||
|
hy2Finalmask.udp ??= [];
|
||||||
|
if (HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm)
|
||||||
|
&& realm is not null)
|
||||||
|
{
|
||||||
|
hy2Finalmask.udp.Add(new Mask4Ray
|
||||||
|
{
|
||||||
|
type = "realm",
|
||||||
|
settings = new MaskSettings4Ray { url = realm.ToUriForFinalmask(), stunServers = realm.StunList },
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
hy2Finalmask.udp =
|
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
[
|
var salamanderSettings = new MaskSettings4Ray
|
||||||
new Mask4Ray
|
{
|
||||||
|
password = protocolExtra.SalamanderPass.TrimEx(),
|
||||||
|
};
|
||||||
|
if (isGecko)
|
||||||
|
{
|
||||||
|
salamanderSettings.packetSize = $"{protocolExtra.GeckoMinPacketSize}-{protocolExtra.GeckoMaxPacketSize}";
|
||||||
|
}
|
||||||
|
hy2Finalmask.udp.Add(new Mask4Ray
|
||||||
{
|
{
|
||||||
type = "salamander",
|
type = "salamander",
|
||||||
settings = new MaskSettings4Ray { password = protocolExtra.SalamanderPass.TrimEx(), }
|
settings = salamanderSettings,
|
||||||
}
|
});
|
||||||
];
|
|
||||||
}
|
}
|
||||||
streamSettings.hysteriaSettings = new()
|
streamSettings.hysteriaSettings = new()
|
||||||
{
|
{
|
||||||
@@ -825,29 +841,7 @@ public partial class CoreConfigV2rayService
|
|||||||
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
|
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello";
|
var (fragmentMask, noiseMask) = BuildFragmentsMasks();
|
||||||
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)
|
foreach (var outbound in actOutboundWithTlsList)
|
||||||
{
|
{
|
||||||
@@ -881,4 +875,76 @@ public partial class CoreConfigV2rayService
|
|||||||
outbound.streamSettings.finalmask = finalMaskJsonObj;
|
outbound.streamSettings.finalmask = finalMaskJsonObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyFinalFragment()
|
||||||
|
{
|
||||||
|
var (fragmentMask, noiseMask) = BuildFragmentsMasks();
|
||||||
|
var actOutboundList = _coreConfig.outbounds.Where(n => n.tag.StartsWith(Global.ProxyTag)).ToList();
|
||||||
|
|
||||||
|
var fragmentFreedom = new Outbounds4Ray()
|
||||||
|
{
|
||||||
|
tag = $"{Global.ProxyTag}-fragment-freedom",
|
||||||
|
protocol = "freedom",
|
||||||
|
streamSettings = new StreamSettings4Ray
|
||||||
|
{
|
||||||
|
finalmask = new Finalmask4Ray
|
||||||
|
{
|
||||||
|
tcp = [fragmentMask],
|
||||||
|
udp = [noiseMask],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var outbound in actOutboundList)
|
||||||
|
{
|
||||||
|
var index = _coreConfig.outbounds.IndexOf(outbound);
|
||||||
|
var originalTag = outbound.tag;
|
||||||
|
var afterTag = $"fragment-{originalTag}";
|
||||||
|
var cloneFragmentFreedom = JsonUtils.DeepCopy(fragmentFreedom);
|
||||||
|
cloneFragmentFreedom.tag = originalTag;
|
||||||
|
cloneFragmentFreedom.streamSettings.sockopt ??= new();
|
||||||
|
cloneFragmentFreedom.streamSettings.sockopt.dialerProxy = afterTag;
|
||||||
|
|
||||||
|
outbound.tag = afterTag;
|
||||||
|
_coreConfig.outbounds.Insert(index, cloneFragmentFreedom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks()
|
||||||
|
{
|
||||||
|
var configPackets = _config.Fragment4RayItem?.Packets.NullIfEmpty() ?? "tlshello";
|
||||||
|
var configLength = _config.Fragment4RayItem?.Length.NullIfEmpty() ?? "50-100";
|
||||||
|
var configDelay = _config.Fragment4RayItem?.Interval.NullIfEmpty() ?? "10-20";
|
||||||
|
var configMaxSplit = _config.Fragment4RayItem?.MaxSplit.NullIfEmpty() ?? "0";
|
||||||
|
|
||||||
|
var maxSplit = 0;
|
||||||
|
var parts = configMaxSplit.Split('-');
|
||||||
|
if (parts.Length > 0 && int.TryParse(parts[0], out var ms))
|
||||||
|
{
|
||||||
|
maxSplit = ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fragmentMask = new Mask4Ray
|
||||||
|
{
|
||||||
|
type = "fragment",
|
||||||
|
settings = new MaskSettings4Ray
|
||||||
|
{
|
||||||
|
packets = configPackets,
|
||||||
|
length = configLength,
|
||||||
|
delay = configDelay,
|
||||||
|
maxSplit = maxSplit,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var noiseMask = new Mask4Ray
|
||||||
|
{
|
||||||
|
type = "noise",
|
||||||
|
settings = new MaskSettings4Ray
|
||||||
|
{
|
||||||
|
length = "10-20",
|
||||||
|
delay = "10-16",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (fragmentMask, noiseMask);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,45 +6,19 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
|
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
|
||||||
{
|
{
|
||||||
var tag = EInboundProtocol.api.ToString();
|
Metrics4Ray metricsObj = new();
|
||||||
Metrics4Ray apiObj = new();
|
|
||||||
Policy4Ray policyObj = new();
|
Policy4Ray policyObj = new();
|
||||||
SystemPolicy4Ray policySystemSetting = new();
|
SystemPolicy4Ray policySystemSetting = new();
|
||||||
|
|
||||||
_coreConfig.stats = new Stats4Ray();
|
_coreConfig.stats = new Stats4Ray();
|
||||||
|
|
||||||
apiObj.tag = tag;
|
metricsObj.listen = $"{Global.Loopback}:{AppManager.Instance.StatePort}";
|
||||||
_coreConfig.metrics = apiObj;
|
_coreConfig.metrics = metricsObj;
|
||||||
|
|
||||||
policySystemSetting.statsOutboundDownlink = true;
|
policySystemSetting.statsOutboundDownlink = true;
|
||||||
policySystemSetting.statsOutboundUplink = true;
|
policySystemSetting.statsOutboundUplink = true;
|
||||||
policyObj.system = policySystemSetting;
|
policyObj.system = policySystemSetting;
|
||||||
_coreConfig.policy = policyObj;
|
_coreConfig.policy = policyObj;
|
||||||
|
|
||||||
if (!_coreConfig.inbounds.Exists(item => item.tag == tag))
|
|
||||||
{
|
|
||||||
Inbounds4Ray apiInbound = new();
|
|
||||||
Inboundsettings4Ray apiInboundSettings = new();
|
|
||||||
apiInbound.tag = tag;
|
|
||||||
apiInbound.listen = Global.Loopback;
|
|
||||||
apiInbound.port = AppManager.Instance.StatePort;
|
|
||||||
apiInbound.protocol = Global.InboundAPIProtocol;
|
|
||||||
apiInboundSettings.address = Global.Loopback;
|
|
||||||
apiInbound.settings = apiInboundSettings;
|
|
||||||
_coreConfig.inbounds.Add(apiInbound);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_coreConfig.routing.rules.Exists(item => item.outboundTag == tag))
|
|
||||||
{
|
|
||||||
RulesItem4Ray apiRoutingRule = new()
|
|
||||||
{
|
|
||||||
inboundTag = new List<string> { tag },
|
|
||||||
outboundTag = tag,
|
|
||||||
type = "field"
|
|
||||||
};
|
|
||||||
|
|
||||||
_coreConfig.routing.rules.Add(apiRoutingRule);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Net.Http.Headers;
|
|||||||
namespace ServiceLib.Services;
|
namespace ServiceLib.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///Download
|
/// Download
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DownloadService
|
public class DownloadService
|
||||||
{
|
{
|
||||||
@@ -13,7 +13,10 @@ public class DownloadService
|
|||||||
|
|
||||||
private static readonly string _tag = "DownloadService";
|
private static readonly string _tag = "DownloadService";
|
||||||
|
|
||||||
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
|
/// <summary>
|
||||||
|
/// Downloads data with the specified proxy and reports progress messages.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -36,6 +39,9 @@ public class DownloadService
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads a file and reports progress through events.
|
||||||
|
/// </summary>
|
||||||
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout)
|
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -64,6 +70,9 @@ public class DownloadService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets redirect target URL without following redirects automatically.
|
||||||
|
/// </summary>
|
||||||
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
|
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
|
||||||
{
|
{
|
||||||
var webRequestHandler = new SocketsHttpHandler
|
var webRequestHandler = new SocketsHttpHandler
|
||||||
@@ -86,11 +95,23 @@ public class DownloadService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to download string content using proxy switch setting.
|
||||||
|
/// </summary>
|
||||||
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
|
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
|
||||||
|
{
|
||||||
|
var webProxy = await GetWebProxy(blProxy);
|
||||||
|
return await TryDownloadString(url, webProxy, userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to download string content with a specified proxy.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15);
|
var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15);
|
||||||
if (result1.IsNotEmpty())
|
if (result1.IsNotEmpty())
|
||||||
{
|
{
|
||||||
return result1;
|
return result1;
|
||||||
@@ -108,7 +129,7 @@ public class DownloadService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15);
|
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15);
|
||||||
if (result2.IsNotEmpty())
|
if (result2.IsNotEmpty())
|
||||||
{
|
{
|
||||||
return result2;
|
return result2;
|
||||||
@@ -128,14 +149,12 @@ public class DownloadService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DownloadString
|
/// Downloads string content via HttpClient.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
|
||||||
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var webProxy = await GetWebProxy(blProxy);
|
|
||||||
var client = new HttpClient(new SocketsHttpHandler()
|
var client = new HttpClient(new SocketsHttpHandler()
|
||||||
{
|
{
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
@@ -172,15 +191,12 @@ public class DownloadService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DownloadString
|
/// Downloads string content via DownloaderHelper.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
|
||||||
private async Task<string?> DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var webProxy = await GetWebProxy(blProxy);
|
|
||||||
|
|
||||||
if (userAgent.IsNullOrEmpty())
|
if (userAgent.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
userAgent = Utils.GetVersion(false);
|
userAgent = Utils.GetVersion(false);
|
||||||
@@ -200,6 +216,9 @@ public class DownloadService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates local SOCKS proxy when proxy switch is enabled.
|
||||||
|
/// </summary>
|
||||||
private async Task<WebProxy?> GetWebProxy(bool blProxy)
|
private async Task<WebProxy?> GetWebProxy(bool blProxy)
|
||||||
{
|
{
|
||||||
if (!blProxy)
|
if (!blProxy)
|
||||||
@@ -215,6 +234,9 @@ public class DownloadService
|
|||||||
return new WebProxy($"socks5://{Global.Loopback}:{port}");
|
return new WebProxy($"socks5://{Global.Loopback}:{port}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the specified TCP endpoint is reachable.
|
||||||
|
/// </summary>
|
||||||
private async Task<bool> SocketCheck(string ip, int port)
|
private async Task<bool> SocketCheck(string ip, int port)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
private static readonly string _tag = "SpeedtestService";
|
private static readonly string _tag = "SpeedtestService";
|
||||||
private readonly Config? _config = config;
|
private readonly Config? _config = config;
|
||||||
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
|
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
|
||||||
private static readonly ConcurrentBag<string> _lstExitLoop = new();
|
private static readonly ConcurrentBag<string> _lstExitLoop = [];
|
||||||
|
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<ProfileItem> selecteds)
|
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
|
||||||
{
|
{
|
||||||
@@ -31,7 +33,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
|
|
||||||
private static bool ShouldStopTest(string exitLoopKey)
|
private static bool ShouldStopTest(string exitLoopKey)
|
||||||
{
|
{
|
||||||
return !_lstExitLoop.Any(p => p == exitLoopKey);
|
return _lstExitLoop.All(p => p != exitLoopKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds)
|
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds)
|
||||||
@@ -44,7 +46,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
switch (actionType)
|
switch (actionType)
|
||||||
{
|
{
|
||||||
case ESpeedActionType.Tcping:
|
case ESpeedActionType.Tcping:
|
||||||
await RunTcpingAsync(lstSelected);
|
await RunTcpingAsync(lstSelected, exitLoopKey);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESpeedActionType.Realping:
|
case ESpeedActionType.Realping:
|
||||||
@@ -133,11 +135,28 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
return lstSelected;
|
return lstSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunTcpingAsync(List<ServerTestItem> selecteds)
|
private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
|
||||||
{
|
{
|
||||||
|
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
|
||||||
|
var lstBatch = GetTestBatchItem(selecteds, pageSize);
|
||||||
|
|
||||||
|
foreach (var lst in lstBatch)
|
||||||
|
{
|
||||||
|
if (ShouldStopTest(exitLoopKey))
|
||||||
|
{
|
||||||
|
await UpdateFunc("", ResUI.SpeedtestingSkip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<Task> tasks = [];
|
List<Task> tasks = [];
|
||||||
foreach (var it in selecteds)
|
|
||||||
|
foreach (var it in lst)
|
||||||
{
|
{
|
||||||
|
if (ShouldStopTest(exitLoopKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tasks.Add(Task.Run(async () =>
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -153,18 +172,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
if (ShouldStopTest(exitLoopKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(_delayInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
|
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
|
||||||
{
|
{
|
||||||
if (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);
|
var lstTest = GetTestBatchItem(lstSelected, pageSize);
|
||||||
|
|
||||||
List<ServerTestItem> lstFailed = new();
|
List<ServerTestItem> lstFailed = [];
|
||||||
foreach (var lst in lstTest)
|
foreach (var lst in lstTest)
|
||||||
{
|
{
|
||||||
var ret = await RunRealPingAsync(lst, exitLoopKey);
|
var ret = await RunRealPingAsync(lst, exitLoopKey);
|
||||||
@@ -172,7 +200,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
{
|
{
|
||||||
lstFailed.AddRange(lst);
|
lstFailed.AddRange(lst);
|
||||||
}
|
}
|
||||||
await Task.Delay(100);
|
await Task.Delay(_delayInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Retest the failed part
|
//Retest the failed part
|
||||||
@@ -210,7 +238,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
}
|
}
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
|
|
||||||
List<Task> tasks = new();
|
List<Task> tasks = [];
|
||||||
foreach (var it in selecteds)
|
foreach (var it in selecteds)
|
||||||
{
|
{
|
||||||
if (!it.AllowTest)
|
if (!it.AllowTest)
|
||||||
@@ -249,11 +277,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
{
|
{
|
||||||
if (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);
|
var lstTest = GetTestBatchItem(lstSelected, pageSize);
|
||||||
|
|
||||||
List<ServerTestItem> lstFailed = new();
|
List<ServerTestItem> lstFailed = [];
|
||||||
foreach (var lst in lstTest)
|
foreach (var lst in lstTest)
|
||||||
{
|
{
|
||||||
var ret = await RunUdpTestAsync(lst, exitLoopKey);
|
var ret = await RunUdpTestAsync(lst, exitLoopKey);
|
||||||
@@ -261,7 +289,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
{
|
{
|
||||||
lstFailed.AddRange(lst);
|
lstFailed.AddRange(lst);
|
||||||
}
|
}
|
||||||
await Task.Delay(100);
|
await Task.Delay(_delayInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Retest the failed part
|
//Retest the failed part
|
||||||
@@ -291,7 +319,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
}
|
}
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
|
|
||||||
List<Task> tasks = new();
|
List<Task> tasks = [];
|
||||||
foreach (var it in selecteds)
|
foreach (var it in selecteds)
|
||||||
{
|
{
|
||||||
if (!it.AllowTest)
|
if (!it.AllowTest)
|
||||||
@@ -329,7 +357,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
{
|
{
|
||||||
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
|
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
|
||||||
var downloadHandle = new DownloadService();
|
var downloadHandle = new DownloadService();
|
||||||
List<Task> tasks = new();
|
List<Task> tasks = [];
|
||||||
foreach (var it in selecteds)
|
foreach (var it in selecteds)
|
||||||
{
|
{
|
||||||
if (ShouldStopTest(exitLoopKey))
|
if (ShouldStopTest(exitLoopKey))
|
||||||
@@ -392,10 +420,23 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
private async Task<int> DoRealPing(ServerTestItem it)
|
private async Task<int> DoRealPing(ServerTestItem it)
|
||||||
{
|
{
|
||||||
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
|
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);
|
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
||||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
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;
|
return responseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +508,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
|
|
||||||
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
|
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
|
||||||
{
|
{
|
||||||
List<List<ServerTestItem>> lstTest = new();
|
List<List<ServerTestItem>> lstTest = [];
|
||||||
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
|
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
|
||||||
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
|
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
|
||||||
|
|
||||||
@@ -491,4 +532,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
ProfileExManager.Instance.SetTestMessage(indexId, speed);
|
ProfileExManager.Instance.SetTestMessage(indexId, speed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateIpInfoFunc(string indexId, string ip)
|
||||||
|
{
|
||||||
|
await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
|||||||
var msgs = new List<string>();
|
var msgs = new List<string>();
|
||||||
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
|
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
|
||||||
{
|
{
|
||||||
|
if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await CheckHasUpdateOnly(type, preRelease);
|
var result = await CheckHasUpdateOnly(type, preRelease);
|
||||||
if (result.Success && result.Version != null)
|
if (result.Success && result.Version != null)
|
||||||
{
|
{
|
||||||
@@ -337,6 +342,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
|||||||
{
|
{
|
||||||
Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64,
|
Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64,
|
||||||
Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64,
|
Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64,
|
||||||
|
Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64,
|
||||||
Architecture.X64 => coreInfo?.DownloadUrlLinux64,
|
Architecture.X64 => coreInfo?.DownloadUrlLinux64,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ public sealed class WindowsJobService : IDisposable
|
|||||||
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
|
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
|
||||||
(uint)length))
|
(uint)length))
|
||||||
{
|
{
|
||||||
throw new Exception(string.Format("Unable to set information. Error: {0}",
|
throw new Exception($"Unable to set information. Error: {Marshal.GetLastWin32Error()}");
|
||||||
Marshal.GetLastWin32Error()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ public class AddGroupServerViewModel : MyReactiveObject
|
|||||||
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
|
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
|
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : Enum.Parse<ECoreType>(CoreType);
|
||||||
if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) ||
|
if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) ||
|
||||||
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
|
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user