Merge branch '2dust:master' into master

This commit is contained in:
freekof 2026-03-01 09:36:33 +08:00 committed by GitHub
commit 8ebc168db8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2888 additions and 3448 deletions

View file

@ -50,7 +50,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPathArm64" dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPathArm64"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0 uses: actions/upload-artifact@v7.0.0
with: with:
name: v2rayN-linux name: v2rayN-linux
path: | path: |
@ -169,7 +169,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Restore build artifacts - name: Restore build artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: v2rayN-linux name: v2rayN-linux
path: ${{ github.workspace }}/v2rayN/Release path: ${{ github.workspace }}/v2rayN/Release
@ -190,7 +190,7 @@ jobs:
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifacts - name: Upload RPM artifacts
uses: actions/upload-artifact@v6.0.0 uses: actions/upload-artifact@v7.0.0
with: with:
name: v2rayN-rpm name: v2rayN-rpm
path: dist/rpm/**/*.rpm path: dist/rpm/**/*.rpm

View file

@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPathArm64 dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0 uses: actions/upload-artifact@v7.0.0
with: with:
name: v2rayN-macos name: v2rayN-macos
path: | path: |

View file

@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64 dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0 uses: actions/upload-artifact@v7.0.0
with: with:
name: v2rayN-windows-desktop name: v2rayN-windows-desktop
path: | path: |

View file

@ -42,7 +42,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64 dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0 uses: actions/upload-artifact@v7.0.0
with: with:
name: v2rayN-windows name: v2rayN-windows
path: | path: |

View file

@ -16,7 +16,7 @@ cp -rf $OutputPath "${PackagePath}/opt/v2rayN"
echo "When this file exists, app will not store configs under this folder" > "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt" echo "When this file exists, app will not store configs under this folder" > "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt"
if [ $Arch = "linux-64" ]; then if [ $Arch = "linux-64" ]; then
Arch2="amd64" Arch2="amd64"
else else
Arch2="arm64" Arch2="arm64"
fi fi
@ -32,9 +32,8 @@ Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26),
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF EOF
cat >"${PackagePath}/DEBIAN/postinst" <<-EOF mkdir -p "${PackagePath}/usr/share/applications"
if [ ! -s /usr/share/applications/v2rayN.desktop ]; then cat >"${PackagePath}/usr/share/applications/v2rayN.desktop" <<-EOF
cat >/usr/share/applications/v2rayN.desktop<<-END
[Desktop Entry] [Desktop Entry]
Name=v2rayN Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
@ -43,10 +42,12 @@ Icon=/opt/v2rayN/v2rayN.png
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Network;Application; Categories=Network;Application;
END EOF
fi
update-desktop-database cat >"${PackagePath}/DEBIAN/postinst" <<-'EOF'
set -e
update-desktop-database || true
exit 0
EOF EOF
sudo chmod 0755 "${PackagePath}/DEBIAN/postinst" sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"

View file

@ -1,45 +1,36 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ====== Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS ====== # Require Red Hat base branch
if [[ -r /etc/os-release ]]; then . /etc/os-release
. /etc/os-release
case "$ID" in case "${ID:-}" in
rhel|rocky|almalinux|fedora|centos) rhel|rocky|almalinux|fedora|centos)
echo "[OK] Detected supported system: $NAME $VERSION_ID" echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}"
;; ;;
*) *)
echo "[ERROR] Unsupported system: $NAME ($ID)." echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})."
echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS." echo "This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
exit 1 exit 1
;; ;;
esac esac
else
echo "[ERROR] Cannot detect system (missing /etc/os-release)." # Kernel version
exit 1 MIN_KERNEL="6.11"
CURRENT_KERNEL="$(uname -r)"
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$CURRENT_KERNEL" | sort -V | head -n1)"
if [[ "$lowest" != "$MIN_KERNEL" ]]; then
echo "Kernel $CURRENT_KERNEL is below $MIN_KERNEL"
exit 1
fi fi
# ======================== Kernel version check (require >= 6.11) ======================= echo "[OK] Kernel $CURRENT_KERNEL verified."
MIN_KERNEL_MAJOR=6
MIN_KERNEL_MINOR=11
KERNEL_FULL=$(uname -r)
KERNEL_MAJOR=$(echo "$KERNEL_FULL" | cut -d. -f1)
KERNEL_MINOR=$(echo "$KERNEL_FULL" | cut -d. -f2)
echo "[INFO] Detected kernel version: $KERNEL_FULL" # Config & Parse arguments
if (( KERNEL_MAJOR < MIN_KERNEL_MAJOR )) || { (( KERNEL_MAJOR == MIN_KERNEL_MAJOR )) && (( KERNEL_MINOR < MIN_KERNEL_MINOR )); }; then
echo "[ERROR] Kernel $KERNEL_FULL is too old. Requires Linux >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
echo "Please upgrade your system or use a newer container (e.g. Fedora 42+, RHEL 10+)."
exit 1
fi
echo "[OK] Kernel version >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
# ===== Config & Parse arguments =========================================================
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
WITH_CORE="both" # Default: bundle both xray+sing-box WITH_CORE="both" # Default: bundle both xray+sing-box
AUTOSTART=0 # 1 = enable system-wide autostart (/etc/xdg/autostart)
FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads
ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target) ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively
@ -55,7 +46,6 @@ if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2;; --with-core) WITH_CORE="${2:-both}"; shift 2;;
--autostart) AUTOSTART=1; shift;;
--xray-ver) XRAY_VER="${2:-}"; shift 2;; --xray-ver) XRAY_VER="${2:-}"; shift 2;;
--singbox-ver) SING_VER="${2:-}"; shift 2;; --singbox-ver) SING_VER="${2:-}"; shift 2;;
--netcore) FORCE_NETCORE=1; shift;; --netcore) FORCE_NETCORE=1; shift;;
@ -69,38 +59,26 @@ done
# Conflict: version number AND --buildfrom cannot be used together # Conflict: version number AND --buildfrom cannot be used together
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
echo "[ERROR] You cannot specify both an explicit version and --buildfrom at the same time." echo "You cannot specify both an explicit version and --buildfrom at the same time."
echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
exit 1 exit 1
fi fi
# ===== Environment check + Dependencies ======================================== # Check and install dependencies
host_arch="$(uname -m)" host_arch="$(uname -m)"
[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; } [[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; }
install_ok=0 install_ok=0
case "$ID" in
rhel|rocky|almalinux|centos)
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1
elif command -v yum >/dev/null 2>&1; then
sudo yum -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
sudo yum -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1
fi
;;
*)
;;
esac
if [[ "$install_ok" -ne 1 ]]; then if command -v dnf >/dev/null 2>&1; then
echo "[WARN] Could not auto-install dependencies for '$ID'. Make sure these are available:" sudo dnf -y install rpm-build rpmdevtools curl unzip tar jq rsync dotnet-sdk-8.0 \
echo " dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on RPM-based distros)" && install_ok=1
fi fi
command -v curl >/dev/null if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$ID'. Make sure these are available:"
echo "dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on Red Hat branch)"
fi
# Root directory # Root directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
@ -119,9 +97,6 @@ if [[ ! -f "$PROJECT" ]]; then
fi fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; } [[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
# Resolve GUI version & auto checkout
VERSION=""
choose_channel() { choose_channel() {
# If --buildfrom provided, map it directly and skip interaction. # If --buildfrom provided, map it directly and skip interaction.
if [[ -n "${BUILD_FROM:-}" ]]; then if [[ -n "${BUILD_FROM:-}" ]]; then
@ -135,60 +110,35 @@ choose_channel() {
# Print menu to stderr and read from /dev/tty so stdout only carries the token. # Print menu to stderr and read from /dev/tty so stdout only carries the token.
local ch="latest" sel="" local ch="latest" sel=""
if [[ -t 0 ]]; then if [[ -t 0 ]]; then
echo "[?] Choose v2rayN release channel:" >&2 echo "[?] Choose v2rayN release channel:" >&2
echo " 1) Latest (stable) [default]" >&2 echo " 1) Latest (stable) [default]" >&2
echo " 2) Pre-release (preview)" >&2 echo " 2) Pre-release (preview)" >&2
echo " 3) Keep current (do nothing)" >&2 echo " 3) Keep current (do nothing)" >&2
printf "Enter 1, 2 or 3 [default 1]: " >&2 printf "Enter 1, 2 or 3 [default 1]: " >&2
if read -r sel </dev/tty; then if read -r sel </dev/tty; then
case "${sel:-}" in case "${sel:-}" in
2) ch="prerelease" ;; 2) ch="prerelease" ;;
3) ch="keep" ;; 3) ch="keep" ;;
*) ch="latest" ;;
esac esac
else
ch="latest"
fi fi
else
ch="latest"
fi fi
echo "$ch" echo "$ch"
} }
get_latest_tag_latest() { get_latest_tag_latest() {
# Resolve /releases/latest → tag_name
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \ curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| grep -Eo '"tag_name":\s*"v?[^"]+"' \ | jq -re '.tag_name' \
| head -n1 \ | sed 's/^v//'
| sed -E 's/.*"tag_name":\s*"v?([^"]+)".*/\1/'
} }
get_latest_tag_prerelease() { get_latest_tag_prerelease() {
# Resolve newest prerelease=true tag; prefer jq, fallback to sed/grep (no awk) curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20" \
local json tag | jq -re 'first(.[] | select(.prerelease == true) | .tag_name)' \
json="$(curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20")" || return 1 | sed 's/^v//'
# 1) Use jq if present
if command -v jq >/dev/null 2>&1; then
tag="$(printf '%s' "$json" \
| jq -r '[.[] | select(.prerelease==true)][0].tag_name' 2>/dev/null \
| sed 's/^v//')" || true
fi
# 2) Fallback to sed/grep only
if [[ -z "${tag:-}" || "${tag:-}" == "null" ]]; then
tag="$(printf '%s' "$json" \
| tr '\n' ' ' \
| sed 's/},[[:space:]]*{/\n/g' \
| grep -m1 -E '"prerelease"[[:space:]]*:[[:space:]]*true' \
| grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/')" || true
fi
[[ -n "${tag:-}" && "${tag:-}" != "null" ]] || return 1
printf '%s\n' "$tag"
} }
git_try_checkout() { git_try_checkout() {
@ -196,11 +146,7 @@ git_try_checkout() {
local want="$1" ref="" local want="$1" ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true git fetch --tags --force --prune --depth=1 || true
if git rev-parse "refs/tags/v${want}" >/dev/null 2>&1; then if git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="v${want}"
elif git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="${want}"
elif git rev-parse --verify "${want}" >/dev/null 2>&1; then
ref="${want}" ref="${want}"
fi fi
if [[ -n "$ref" ]]; then if [[ -n "$ref" ]]; then
@ -216,88 +162,56 @@ git_try_checkout() {
return 1 return 1
} }
apply_channel_or_keep() {
local ch="$1" tag
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0+git')"
VERSION="${VERSION#v}"
return 0
fi
echo "[*] Resolving ${ch} tag from GitHub releases..."
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
}
if git rev-parse --git-dir >/dev/null 2>&1; then if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then if [[ -n "${VERSION_ARG:-}" ]]; then
echo "[*] Trying to switch v2rayN repo to version: ${VERSION_ARG}" clean_ver="${VERSION_ARG#v}"
if git_try_checkout "${VERSION_ARG#v}"; then if git_try_checkout "$clean_ver"; then
VERSION="${VERSION_ARG#v}" VERSION="$clean_ver"
else else
echo "[WARN] Tag '${VERSION_ARG}' not found." echo "[WARN] Tag '${VERSION_ARG}' not found."
ch="$(choose_channel)" ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then apply_channel_or_keep "$ch"
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
fi fi
else else
ch="$(choose_channel)" ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then apply_channel_or_keep "$ch"
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
fi fi
else else
echo "[WARN] Current directory is not a git repo; cannot checkout version. Proceeding on current tree." echo "Current directory is not a git repo; proceeding on current tree."
VERSION="${VERSION_ARG:-}" VERSION="${VERSION_ARG:-0.0.0}"
if [[ -z "$VERSION" ]]; then
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
fi
VERSION="${VERSION#v}"
fi fi
VERSION="${VERSION#v}"
echo "[*] GUI version resolved as: ${VERSION}" echo "[*] GUI version resolved as: ${VERSION}"
# ===== Helpers for core/rules download (use RID_DIR for arch sync) ===================== # Helpers for core
download_xray() { download_xray() {
# Download Xray core and install to outdir/xray # Download Xray core
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir" mkdir -p "$outdir"
if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi
if [[ -z "$ver" ]]; then if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \ ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
@ -316,10 +230,9 @@ download_xray() {
} }
download_singbox() { download_singbox() {
# Download sing-box core and install to outdir/sing-box # Download sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir" mkdir -p "$outdir"
if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi
if [[ -z "$ver" ]]; then if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \ ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
@ -339,7 +252,7 @@ download_singbox() {
install -Dm755 "$bin" "$outdir/sing-box" install -Dm755 "$bin" "$outdir/sing-box"
} }
# Move geo files to a unified path: outroot/bin # Move geo files to outroot/bin
unify_geo_layout() { unify_geo_layout() {
local outroot="$1" local outroot="$1"
mkdir -p "$outroot/bin" mkdir -p "$outroot/bin"
@ -351,18 +264,13 @@ unify_geo_layout() {
"geoip.metadb" \ "geoip.metadb" \
) )
for n in "${names[@]}"; do for n in "${names[@]}"; do
# If file exists under bin/xray/, move it up to bin/
if [[ -f "$outroot/bin/xray/$n" ]]; then if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n" mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi
done done
} }
# Download geo/rule assets; then unify to bin/ # Download geo/rule assets
download_geo_assets() { download_geo_assets() {
local outroot="$1" local outroot="$1"
local bin_dir="$outroot/bin" local bin_dir="$outroot/bin"
@ -396,7 +304,7 @@ download_geo_assets() {
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true "https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done done
# Unify to bin/ # Unify to bin
unify_geo_layout "$outroot" unify_geo_layout "$outroot"
} }
@ -427,7 +335,7 @@ download_v2rayn_bundle() {
local nested_dir local nested_dir
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)" nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
if [[ -n "${nested_dir:-}" && -d "$nested_dir/bin" ]]; then if [[ -n "$nested_dir" && -d "$nested_dir/bin" ]]; then
mkdir -p "$outroot/bin" mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/" rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir" rm -rf "$nested_dir"
@ -451,7 +359,7 @@ build_for_arch() {
case "$short" in case "$short" in
x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;; x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;;
arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;; arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;;
*) echo "[ERROR] Unknown arch '$short' (use x64|arm64)"; return 1;; *) echo "Unknown arch '$short' (use x64|arm64)"; return 1;;
esac esac
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
@ -464,8 +372,7 @@ build_for_arch() {
dotnet publish "$PROJECT" \ dotnet publish "$PROJECT" \
-c Release -r "$rid" \ -c Release -r "$rid" \
-p:PublishSingleFile=false \ -p:PublishSingleFile=false \
-p:SelfContained=true \ -p:SelfContained=true
-p:IncludeNativeLibrariesForSelfExtract=true
# Per-arch variables (scoped) # Per-arch variables (scoped)
local RID_DIR="$rid" local RID_DIR="$rid"
@ -502,28 +409,28 @@ build_for_arch() {
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box" mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
# Bundle / cores per-arch # Bundle / cores per-arch
fetch_separate_cores_and_rules() {
local outroot="$1"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
if [[ "$FORCE_NETCORE" -eq 0 ]]; then if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then
echo "[*] Using v2rayN bundle archive." echo "[*] Using v2rayN bundle archive."
else else
echo "[*] Bundle failed, fallback to separate core + rules." echo "[*] Bundle failed, fallback to separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT"
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
fi fi
else else
echo "[*] --netcore specified: use separate core + rules." echo "[*] --netcore specified: use separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT"
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
fi fi
# Tarball # Tarball
@ -577,12 +484,6 @@ https://github.com/2dust/v2rayN
install -dm0755 %{buildroot}/opt/v2rayN install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/ cp -a * %{buildroot}/opt/v2rayN/
install -dm0755 %{buildroot}%{_sysconfdir}/sudoers.d
cat > %{buildroot}%{_sysconfdir}/sudoers.d/v2rayn-mihomo-deny << 'EOF'
ALL ALL=(ALL) !/home/*/.local/share/v2rayN/bin/mihomo/mihomo
EOF
chmod 0440 %{buildroot}%{_sysconfdir}/sudoers.d/v2rayn-mihomo-deny
# Launcher (prefer native ELF first, then DLL fallback) # Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir} install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF' cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
@ -636,47 +537,13 @@ fi
/opt/v2rayN /opt/v2rayN
%{_datadir}/applications/v2rayn.desktop %{_datadir}/applications/v2rayn.desktop
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png %{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
%config(noreplace) /etc/sudoers.d/v2rayn-mihomo-deny
SPEC SPEC
# Autostart injection (inside %install) and %files entry
if [[ "$AUTOSTART" -eq 1 ]]; then
awk '
BEGIN{ins=0}
/^%post$/ && !ins {
print "# --- Autostart (.desktop) ---"
print "install -dm0755 %{buildroot}/etc/xdg/autostart"
print "cat > %{buildroot}/etc/xdg/autostart/v2rayn.desktop << '\''EOF'\''"
print "[Desktop Entry]"
print "Type=Application"
print "Name=v2rayN (Autostart)"
print "Exec=v2rayn"
print "X-GNOME-Autostart-enabled=true"
print "NoDisplay=false"
print "EOF"
ins=1
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
awk '
BEGIN{infiles=0; done=0}
/^%files$/ {infiles=1}
infiles && done==0 && $0 ~ /%{_datadir}\/icons\/hicolor\/256x256\/apps\/v2rayn\.png/ {
print
print "%config(noreplace) /etc/xdg/autostart/v2rayn.desktop"
done=1
next
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
fi
# Replace placeholders # Replace placeholders
sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE" sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE" sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE"
# Build RPM for this arch (force rpm --target to match compile arch) # Build RPM for this arch
rpmbuild -ba "$SPECFILE" --target "$rpm_target" rpmbuild -ba "$SPECFILE" --target "$rpm_target"
echo "Build done for $short. RPM at:" echo "Build done for $short. RPM at:"
@ -690,33 +557,18 @@ SPEC
# ===== Arch selection and build orchestration ========================================= # ===== Arch selection and build orchestration =========================================
case "${ARCH_OVERRIDE:-}" in case "${ARCH_OVERRIDE:-}" in
"") all) targets=(x64 arm64); BUILT_ALL=1 ;;
# No --arch: use host architecture x64|amd64) targets=(x64) ;;
if [[ "$host_arch" == "aarch64" ]]; then arm64|aarch64) targets=(arm64) ;;
build_for_arch arm64 "") targets=($([[ "$host_arch" == "aarch64" ]] && echo arm64 || echo x64)) ;;
else *) echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."; exit 1 ;;
build_for_arch x64
fi
;;
x64|amd64)
build_for_arch x64
;;
arm64|aarch64)
build_for_arch arm64
;;
all)
BUILT_ALL=1
# Build x64 and arm64 separately; each package contains its own arch-only binaries.
build_for_arch x64
build_for_arch arm64
;;
*)
echo "[ERROR] Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."
exit 1
;;
esac esac
# ===== Final summary if building both arches ========================================== for arch in "${targets[@]}"; do
build_for_arch "$arch"
done
# Print Both arches information
if [[ "$BUILT_ALL" -eq 1 ]]; then if [[ "$BUILT_ALL" -eq 1 ]]; then
echo "" echo ""
echo "================ Build Summary (both architectures) ================" echo "================ Build Summary (both architectures) ================"
@ -725,7 +577,7 @@ if [[ "$BUILT_ALL" -eq 1 ]]; then
echo "$rp" echo "$rp"
done done
else else
echo "[WARN] No RPMs detected in summary (check build logs above)." echo "No RPMs detected in summary (check build logs above)."
fi fi
echo "===================================================================" echo "===================================================================="
fi fi

View file

@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.18.0</Version> <Version>7.19.0</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View file

@ -5,13 +5,13 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.0" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.11" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.11" /> <PackageVersion Include="Avalonia.Desktop" Version="11.3.12" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.11" /> <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.8" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageVersion Include="CliWrap" Version="3.10.0" /> <PackageVersion Include="CliWrap" Version="3.10.0" />
<PackageVersion Include="Downloader" Version="4.0.3" /> <PackageVersion Include="Downloader" Version="4.1.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.1.1" /> <PackageVersion Include="MessageBox.Avalonia" Version="3.3.1.1" />
@ -19,9 +19,9 @@
<PackageVersion Include="ReactiveUI" Version="22.3.1" /> <PackageVersion Include="ReactiveUI" Version="22.3.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="22.3.1" /> <PackageVersion Include="ReactiveUI.WPF" Version="22.3.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7.2" /> <PackageVersion Include="Semi.Avalonia" Version="11.3.7.3" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.2" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.0" /> <PackageVersion Include="NLog" Version="6.1.0" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" /> <PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />

View file

@ -629,12 +629,7 @@ public class Utils
{ {
try try
{ {
List<IPEndPoint> lstIpEndPoints = new(); var (lstIpEndPoints, lstTcpConns) = GetActiveNetworkInfo();
List<TcpConnectionInformation> lstTcpConns = new();
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
if (lstIpEndPoints?.FindIndex(it => it.Port == port) >= 0) if (lstIpEndPoints?.FindIndex(it => it.Port == port) >= 0)
{ {
@ -676,6 +671,27 @@ public class Utils
return 59090; return 59090;
} }
public static (List<IPEndPoint> endpoints, List<TcpConnectionInformation> connections) GetActiveNetworkInfo()
{
var endpoints = new List<IPEndPoint>();
var connections = new List<TcpConnectionInformation>();
try
{
var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
endpoints.AddRange(ipGlobalProperties.GetActiveTcpListeners());
endpoints.AddRange(ipGlobalProperties.GetActiveUdpListeners());
connections.AddRange(ipGlobalProperties.GetActiveTcpConnections());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return (endpoints, connections);
}
#endregion Speed Test #endregion Speed Test
#region Miscellaneous #region Miscellaneous

View file

@ -15,7 +15,6 @@ public class Global
public const string CoreConfigFileName = "config.json"; public const string CoreConfigFileName = "config.json";
public const string CorePreConfigFileName = "configPre.json"; public const string CorePreConfigFileName = "configPre.json";
public const string CoreSpeedtestConfigFileName = "configTest{0}.json"; public const string CoreSpeedtestConfigFileName = "configTest{0}.json";
public const string CoreMultipleLoadConfigFileName = "configMultipleLoad.json";
public const string ClashMixinConfigFileName = "Mixin.yaml"; public const string ClashMixinConfigFileName = "Mixin.yaml";
public const string NamespaceSample = "ServiceLib.Sample."; public const string NamespaceSample = "ServiceLib.Sample.";
@ -88,7 +87,6 @@ public class Global
public const string SingboxLocalDNSTag = "local_local"; public const string SingboxLocalDNSTag = "local_local";
public const string SingboxHostsDNSTag = "hosts_dns"; public const string SingboxHostsDNSTag = "hosts_dns";
public const string SingboxFakeDNSTag = "fake_dns"; public const string SingboxFakeDNSTag = "fake_dns";
public const string SingboxEchDNSTag = "ech_dns";
public const int Hysteria2DefaultHopInt = 10; public const int Hysteria2DefaultHopInt = 10;

View file

@ -24,6 +24,7 @@ global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;
global using ServiceLib.Events; global using ServiceLib.Events;
global using ServiceLib.Handler; global using ServiceLib.Handler;
global using ServiceLib.Handler.Builder;
global using ServiceLib.Handler.Fmt; global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy; global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper; global using ServiceLib.Helper;

View file

@ -0,0 +1,331 @@
namespace ServiceLib.Handler.Builder;
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
{
public bool Success => ValidatorResult.Success;
}
public class CoreConfigContextBuilder
{
/// <summary>
/// Builds a <see cref="CoreConfigContext"/> for the given node, resolves its proxy map,
/// and processes outbound nodes referenced by routing rules.
/// </summary>
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
{
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var context = new CoreConfigContext()
{
Node = node,
RunCoreType = runCoreType,
AllProxiesMap = [],
AppConfig = config,
FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType),
IsTunEnabled = config.TunModeItem.EnableTun,
SimpleDnsItem = config.SimpleDNSItem,
ProtectDomainList = [],
TunProtectSsPort = 0,
ProxyRelaySsPort = 0,
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
};
var validatorResult = NodeValidatorResult.Empty();
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
if (!nodeValidatorResult.Success)
{
return new CoreConfigContextBuilderResult(context, nodeValidatorResult);
}
context = context with { Node = actNode };
validatorResult.Warnings.AddRange(nodeValidatorResult.Warnings);
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
{
if (ruleItem.OutboundTag.IsNullOrEmpty())
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleEmptyOutboundTag, ruleItem.Remarks));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
if (ruleOutboundNode == null)
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
var (actRuleNode, ruleNodeValidatorResult) = await ResolveNodeAsync(context, ruleOutboundNode, false);
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w =>
string.Format(ResUI.MsgRoutingRuleOutboundNodeWarning, ruleItem.Remarks, ruleItem.OutboundTag, w)));
if (!ruleNodeValidatorResult.Success)
{
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e)));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
}
}
return new CoreConfigContextBuilderResult(context, validatorResult);
}
/// <summary>
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
/// Returns the effective (possibly replaced) node and the validation result.
/// </summary>
public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context,
ProfileItem node,
bool includeSubChain = true)
{
if (node.IndexId.IsNullOrEmpty())
{
return (node, NodeValidatorResult.Empty());
}
if (includeSubChain)
{
var (virtualChainNode, chainValidatorResult) = await BuildSubscriptionChainNodeAsync(node);
if (virtualChainNode != null)
{
context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode;
var (resolvedNode, resolvedResult) = await ResolveNodeAsync(context, virtualChainNode, false);
resolvedResult.Warnings.InsertRange(0, chainValidatorResult.Warnings);
return (resolvedNode, resolvedResult);
}
// Chain not built but warnings may still exist (e.g. missing profiles)
if (chainValidatorResult.Warnings.Count > 0)
{
var fillResult = await RegisterNodeAsync(context, node);
fillResult.Warnings.InsertRange(0, chainValidatorResult.Warnings);
return (node, fillResult);
}
}
var registerResult = await RegisterNodeAsync(context, node);
return (node, registerResult);
}
/// <summary>
/// If the node's subscription defines prev/next profiles, creates a virtual
/// <see cref="EConfigType.ProxyChain"/> node that wraps them together.
/// 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.
/// </summary>
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node)
{
var result = NodeValidatorResult.Empty();
if (node.Subid.IsNullOrEmpty())
{
return (null, result);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem == null)
{
return (null, result);
}
ProfileItem? prevNode = null;
ProfileItem? nextNode = null;
if (!subItem.PrevProfile.IsNullOrEmpty())
{
prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode == null)
{
result.Warnings.Add(string.Format(ResUI.MsgSubscriptionPrevProfileNotFound, subItem.PrevProfile));
}
}
if (!subItem.NextProfile.IsNullOrEmpty())
{
nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode == null)
{
result.Warnings.Add(string.Format(ResUI.MsgSubscriptionNextProfileNotFound, subItem.NextProfile));
}
}
if (prevNode is null && nextNode is null)
{
return (null, result);
}
// Build new proxy chain node
var chainNode = new ProfileItem()
{
IndexId = $"inner-{Utils.GetGuid(false)}",
ConfigType = EConfigType.ProxyChain,
CoreType = node.CoreType ?? ECoreType.Xray,
};
List<string?> childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId];
var chainExtraItem = chainNode.GetProtocolExtra() with
{
GroupType = chainNode.ConfigType.ToString(),
ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())),
};
chainNode.SetProtocolExtra(chainExtraItem);
return (chainNode, result);
}
/// <summary>
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync"/> or
/// <see cref="RegisterSingleNodeAsync"/> based on the node's config type.
/// </summary>
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
{
if (node.ConfigType.IsGroupType())
{
return await RegisterGroupNodeAsync(context, node);
}
else
{
return RegisterSingleNodeAsync(context, node);
}
}
/// <summary>
/// Validates a single (non-group) node and, on success, adds it to the proxy map
/// and records any domain addresses that should bypass the proxy.
/// </summary>
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
{
if (node.ConfigType.IsGroupType())
{
return NodeValidatorResult.Empty();
}
var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType);
if (!nodeValidatorResult.Success)
{
return nodeValidatorResult;
}
context.AllProxiesMap[node.IndexId] = node;
var address = node.Address;
if (Utils.IsDomain(address))
{
context.ProtectDomainList.Add(address);
}
if (!node.EchConfigList.IsNullOrEmpty())
{
var echQuerySni = node.Sni;
if (node.StreamSecurity == Global.StreamSecurity
&& node.EchConfigList?.Contains("://") == true)
{
var idx = node.EchConfigList.IndexOf('+');
echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni;
}
if (Utils.IsDomain(echQuerySni))
{
context.ProtectDomainList.Add(echQuerySni);
}
}
return nodeValidatorResult;
}
/// <summary>
/// Entry point for registering a group node. Initialises the visited/ancestor sets
/// and delegates to <see cref="TraverseGroupNodeAsync"/>.
/// </summary>
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
ProfileItem node)
{
if (!node.ConfigType.IsGroupType())
{
return NodeValidatorResult.Empty();
}
HashSet<string> ancestors = [node.IndexId];
HashSet<string> globalVisited = [node.IndexId];
return await TraverseGroupNodeAsync(context, node, globalVisited, ancestors);
}
/// <summary>
/// Recursively walks the children of a group node, registering valid leaf nodes
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup"/> and
/// deduplicates shared nodes via <paramref name="globalVisitedGroup"/>.
/// </summary>
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
CoreConfigContext context,
ProfileItem node,
HashSet<string> globalVisitedGroup,
HashSet<string> ancestorsGroup)
{
var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node);
List<string> childIndexIdList = [];
var childNodeValidatorResult = NodeValidatorResult.Empty();
foreach (var childNode in groupChildList)
{
if (ancestorsGroup.Contains(childNode.IndexId))
{
childNodeValidatorResult.Errors.Add(
string.Format(ResUI.MsgGroupCycleDependency, node.Remarks, childNode.Remarks));
continue;
}
if (globalVisitedGroup.Contains(childNode.IndexId))
{
childIndexIdList.Add(childNode.IndexId);
continue;
}
if (!childNode.ConfigType.IsGroupType())
{
var childNodeResult = RegisterSingleNodeAsync(context, childNode);
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>
string.Format(ResUI.MsgGroupChildNodeError, node.Remarks, childNode.Remarks, e)));
if (!childNodeResult.Success)
{
continue;
}
globalVisitedGroup.Add(childNode.IndexId);
childIndexIdList.Add(childNode.IndexId);
continue;
}
var newAncestorsGroup = new HashSet<string>(ancestorsGroup) { childNode.IndexId };
var childGroupResult =
await TraverseGroupNodeAsync(context, childNode, globalVisitedGroup, newAncestorsGroup);
childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w =>
string.Format(ResUI.MsgGroupChildGroupNodeWarning, node.Remarks, childNode.Remarks, w)));
childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e =>
string.Format(ResUI.MsgGroupChildGroupNodeError, node.Remarks, childNode.Remarks, e)));
if (!childGroupResult.Success)
{
continue;
}
globalVisitedGroup.Add(childNode.IndexId);
childIndexIdList.Add(childNode.IndexId);
}
if (childIndexIdList.Count == 0)
{
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
return childNodeValidatorResult;
}
else
{
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
childNodeValidatorResult.Errors.Clear();
}
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), });
context.AllProxiesMap[node.IndexId] = node;
return childNodeValidatorResult;
}
}

View file

@ -0,0 +1,175 @@
namespace ServiceLib.Handler.Builder;
public record NodeValidatorResult(List<string> Errors, List<string> Warnings)
{
public bool Success => Errors.Count == 0;
public static NodeValidatorResult Empty()
{
return new NodeValidatorResult([], []);
}
}
public class NodeValidator
{
// Static validator rules
private static readonly HashSet<string> SingboxUnsupportedTransports =
[nameof(ETransport.kcp), nameof(ETransport.xhttp)];
private static readonly HashSet<EConfigType> SingboxTransportSupportedProtocols =
[EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
private static readonly HashSet<string> SingboxShadowsocksAllowedTransports =
[nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)];
public static NodeValidatorResult Validate(ProfileItem item, ECoreType coreType)
{
var v = new ValidationContext();
ValidateNodeAndCoreSupport(item, coreType, v);
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)
{
if (item.ConfigType is EConfigType.Custom)
{
return;
}
if (item.ConfigType.IsGroupType())
{
// Group logic is handled in ValidateGroupNode
return;
}
// Basic Property Validation
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address"));
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port"));
// Network & Core Logic
var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
var transportError = ValidateSingboxTransport(item.ConfigType, net);
if (transportError != null)
v.Error(transportError);
if (!Global.SingboxSupportConfigType.Contains(item.ConfigType))
{
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType));
}
}
else if (coreType is ECoreType.Xray)
{
if (!Global.XraySupportConfigType.Contains(item.ConfigType))
{
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType));
}
}
// Protocol Specifics
var protocolExtra = item.GetProtocolExtra();
switch (item.ConfigType)
{
case EConfigType.VMess:
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
string.Format(ResUI.MsgInvalidProperty, "Password"));
break;
case EConfigType.VLESS:
v.Assert(
!item.Password.IsNullOrEmpty()
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
string.Format(ResUI.MsgInvalidProperty, "Password")
);
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
string.Format(ResUI.MsgInvalidProperty, "Flow"));
break;
case EConfigType.Shadowsocks:
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password"));
v.Assert(
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
string.Format(ResUI.MsgInvalidProperty, "SsMethod"));
break;
}
// TLS & Security
if (item.StreamSecurity == Global.StreamSecurity)
{
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 &&
!item.CertSha.IsNullOrEmpty())
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
{
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey"));
}
if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty())
{
if (JsonUtils.ParseJson(item.Extra) is null)
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra"));
}
}
}
private static string? ValidateSingboxTransport(EConfigType configType, string net)
{
// sing-box does not support xhttp / kcp transports
if (SingboxUnsupportedTransports.Contains(net))
{
return string.Format(ResUI.MsgCoreNotSupportNetwork, nameof(ECoreType.sing_box), net);
}
// sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks
if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp))
{
return string.Format(ResUI.MsgCoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
// sing-box shadowsocks only supports tcp/ws/quic transports
if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net))
{
return string.Format(ResUI.MsgCoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
return null;
}
}

View file

@ -254,6 +254,7 @@ public static class ConfigHandler
item.CertSha = profileItem.CertSha; item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList; item.EchConfigList = profileItem.EchConfigList;
item.EchForceQuery = profileItem.EchForceQuery; item.EchForceQuery = profileItem.EchForceQuery;
item.Finalmask = profileItem.Finalmask;
item.ProtoExtra = profileItem.ProtoExtra; item.ProtoExtra = profileItem.ProtoExtra;
} }
@ -1122,6 +1123,7 @@ public static class ConfigHandler
&& AreEqual(o.Fingerprint, n.Fingerprint) && AreEqual(o.Fingerprint, n.Fingerprint)
&& AreEqual(o.PublicKey, n.PublicKey) && AreEqual(o.PublicKey, n.PublicKey)
&& AreEqual(o.ShortId, n.ShortId) && AreEqual(o.ShortId, n.ShortId)
&& AreEqual(o.Finalmask, n.Finalmask)
&& (!remarks || o.Remarks == n.Remarks); && (!remarks || o.Remarks == n.Remarks);
static bool AreEqual(string? a, string? b) static bool AreEqual(string? a, string? b)
@ -1231,44 +1233,65 @@ public static class ConfigHandler
/// <param name="node">Server node that might need pre-SOCKS</param> /// <param name="node">Server node that might need pre-SOCKS</param>
/// <param name="coreType">Core type being used</param> /// <param name="coreType">Core type being used</param>
/// <returns>A SOCKS profile item or null if not needed</returns> /// <returns>A SOCKS profile item or null if not needed</returns>
public static async Task<ProfileItem?> GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
{ {
if (node.ConfigType != EConfigType.Custom || !(node.PreSocksPort > 0))
{
return null;
}
ProfileItem? itemSocks = null; ProfileItem? itemSocks = null;
if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) var preCoreType = AppManager.Instance.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem()
{ {
var tun2SocksAddress = node.Address; CoreType = preCoreType,
if (node.ConfigType.IsGroupType()) ConfigType = EConfigType.SOCKS,
{ Address = Global.Loopback,
var lstAddresses = (await GroupProfileManager.GetAllChildDomainAddresses(node)).ToList(); Port = node.PreSocksPort.Value,
if (lstAddresses.Count > 0) };
{
tun2SocksAddress = Utils.List2String(lstAddresses);
}
}
itemSocks = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
SpiderX = tun2SocksAddress, // Tun2SocksAddress
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
};
}
else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)
{
var preCoreType = AppManager.Instance.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem()
{
CoreType = preCoreType,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
Port = node.PreSocksPort.Value,
};
}
await Task.CompletedTask;
return itemSocks; return itemSocks;
} }
public static CoreConfigContext? GetPreSocksCoreConfigContext(CoreConfigContext nodeContext)
{
var config = nodeContext.AppConfig;
var node = nodeContext.Node;
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var preSocksItem = GetPreSocksItem(config, node, coreType);
if (preSocksItem != null)
{
return nodeContext with { Node = preSocksItem, };
}
if ((!nodeContext.IsTunEnabled)
|| coreType != ECoreType.Xray
|| node.ConfigType == EConfigType.Custom)
{
return null;
}
var tunProtectSsPort = Utils.GetFreePort();
var proxyRelaySsPort = Utils.GetFreePort();
var preItem = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.Shadowsocks,
Address = Global.Loopback,
Port = proxyRelaySsPort,
Password = Global.None,
};
preItem.SetProtocolExtra(preItem.GetProtocolExtra() with
{
SsMethod = Global.None,
});
var preContext = nodeContext with
{
Node = preItem,
TunProtectSsPort = tunProtectSsPort,
ProxyRelaySsPort = proxyRelaySsPort,
};
return preContext;
}
/// <summary> /// <summary>
/// Remove servers with invalid test results (timeout) /// Remove servers with invalid test results (timeout)
/// Useful for cleaning up subscription lists /// Useful for cleaning up subscription lists

View file

@ -7,27 +7,27 @@ public static class CoreConfigHandler
{ {
private static readonly string _tag = "CoreConfigHandler"; private static readonly string _tag = "CoreConfigHandler";
public static async Task<RetResult> GenerateClientConfig(ProfileItem node, string? fileName) public static async Task<RetResult> GenerateClientConfig(CoreConfigContext context, string? fileName)
{ {
var config = AppManager.Instance.Config; var config = AppManager.Instance.Config;
var result = new RetResult(); var result = new RetResult();
var node = context.Node;
if (node.ConfigType == EConfigType.Custom) if (node.ConfigType == EConfigType.Custom)
{ {
result = node.CoreType switch result = node.CoreType switch
{ {
ECoreType.mihomo => await new CoreConfigClashService(config).GenerateClientCustomConfig(node, fileName), ECoreType.mihomo => await new CoreConfigClashService(config).GenerateClientCustomConfig(node, fileName),
ECoreType.sing_box => await new CoreConfigSingboxService(config).GenerateClientCustomConfig(node, fileName),
_ => await GenerateClientCustomConfig(node, fileName) _ => await GenerateClientCustomConfig(node, fileName)
}; };
} }
else if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box) else if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
{ {
result = await new CoreConfigSingboxService(config).GenerateClientConfigContent(node); result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
} }
else else
{ {
result = await new CoreConfigV2rayService(config).GenerateClientConfigContent(node); result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
} }
if (result.Success != true) if (result.Success != true)
{ {
@ -93,13 +93,31 @@ public static class CoreConfigHandler
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, string fileName, List<ServerTestItem> selecteds, ECoreType coreType) public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, string fileName, List<ServerTestItem> selecteds, ECoreType coreType)
{ {
var result = new RetResult(); var result = new RetResult();
var dummyNode = new ProfileItem
{
CoreType = coreType
};
var builderResult = await CoreConfigContextBuilder.Build(config, dummyNode);
var context = builderResult.Context;
var ids = selecteds.Where(serverTestItem => !serverTestItem.IndexId.IsNullOrEmpty())
.Select(serverTestItem => serverTestItem.IndexId);
var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids);
foreach (var node in nodes)
{
var (actNode, _) = await CoreConfigContextBuilder.ResolveNodeAsync(context, node, true);
if (node.IndexId == actNode.IndexId)
{
continue;
}
context.ServerTestItemMap[node.IndexId] = actNode.IndexId;
}
if (coreType == ECoreType.sing_box) if (coreType == ECoreType.sing_box)
{ {
result = await new CoreConfigSingboxService(config).GenerateClientSpeedtestConfig(selecteds); result = new CoreConfigSingboxService(context).GenerateClientSpeedtestConfig(selecteds);
} }
else if (coreType == ECoreType.Xray) else if (coreType == ECoreType.Xray)
{ {
result = await new CoreConfigV2rayService(config).GenerateClientSpeedtestConfig(selecteds); result = new CoreConfigV2rayService(context).GenerateClientSpeedtestConfig(selecteds);
} }
if (result.Success != true) if (result.Success != true)
{ {
@ -109,20 +127,21 @@ public static class CoreConfigHandler
return result; return result;
} }
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, ProfileItem node, ServerTestItem testItem, string fileName) public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, CoreConfigContext context, ServerTestItem testItem, string fileName)
{ {
var result = new RetResult(); var result = new RetResult();
var node = context.Node;
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest); var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
var port = Utils.GetFreePort(initPort + testItem.QueueNum); var port = Utils.GetFreePort(initPort + testItem.QueueNum);
testItem.Port = port; testItem.Port = port;
if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box) if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
{ {
result = await new CoreConfigSingboxService(config).GenerateClientSpeedtestConfig(node, port); result = new CoreConfigSingboxService(context).GenerateClientSpeedtestConfig(port);
} }
else else
{ {
result = await new CoreConfigV2rayService(config).GenerateClientSpeedtestConfig(node, port); result = new CoreConfigV2rayService(context).GenerateClientSpeedtestConfig(port);
} }
if (result.Success != true) if (result.Success != true)
{ {

View file

@ -73,6 +73,19 @@ public class BaseFmt
{ {
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha)); dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
} }
if (item.Finalmask.IsNotEmpty())
{
var node = JsonUtils.ParseJson(item.Finalmask);
var finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
: item.Finalmask;
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
}
dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
@ -214,6 +227,24 @@ public class BaseFmt
item.EchConfigList = GetQueryDecoded(query, "ech"); item.EchConfigList = GetQueryDecoded(query, "ech");
item.CertSha = GetQueryDecoded(query, "pcs"); item.CertSha = GetQueryDecoded(query, "pcs");
var finalmaskDecoded = GetQueryDecoded(query, "fm");
if (finalmaskDecoded.IsNotEmpty())
{
var node = JsonUtils.ParseJson(finalmaskDecoded);
item.Finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
: finalmaskDecoded;
}
else
{
item.Finalmask = string.Empty;
}
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1")) if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{ {
item.AllowInsecure = Global.AllowInsecure.First(); item.AllowInsecure = Global.AllowInsecure.First();

View file

@ -66,7 +66,7 @@ public class Hysteria2Fmt : BaseFmt
if (!item.CertSha.IsNullOrEmpty()) if (!item.CertSha.IsNullOrEmpty())
{ {
var sha = item.CertSha; var sha = item.CertSha;
var idx = sha.IndexOf('~'); var idx = sha.IndexOf(',');
if (idx > 0) if (idx > 0)
{ {
sha = sha[..idx]; sha = sha[..idx];

View file

@ -24,13 +24,13 @@ public class DownloaderHelper
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
Timeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
Headers = headers, Headers = headers,
UserAgent = userAgent, UserAgent = userAgent,
Timeout = timeout * 1000, ConnectTimeout = timeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
@ -62,11 +62,11 @@ public class DownloaderHelper
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
Timeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
Timeout= timeout * 1000, ConnectTimeout= timeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };
@ -139,11 +139,11 @@ public class DownloaderHelper
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
Timeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration =
{ {
Timeout= timeout * 1000, ConnectTimeout= timeout * 1000,
Proxy = webProxy Proxy = webProxy
} }
}; };

View file

@ -1,352 +0,0 @@
namespace ServiceLib.Manager;
/// <summary>
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
/// </summary>
public class ActionPrecheckManager
{
private static readonly Lazy<ActionPrecheckManager> _instance = new();
public static ActionPrecheckManager Instance => _instance.Value;
// sing-box supported transports for different protocol types
private static readonly HashSet<string> SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)];
private static readonly HashSet<EConfigType> SingboxTransportSupportedProtocols =
[EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
private static readonly HashSet<string> SingboxShadowsocksAllowedTransports =
[nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)];
public async Task<List<string>> Check(string? indexId)
{
if (indexId.IsNullOrEmpty())
{
return [ResUI.PleaseSelectServer];
}
var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null)
{
return [ResUI.PleaseSelectServer];
}
return await Check(item);
}
public async Task<List<string>> Check(ProfileItem? item)
{
if (item is null)
{
return [ResUI.PleaseSelectServer];
}
var errors = new List<string>();
errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item));
errors.AddRange(await ValidateRelatedNodesExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateCurrentNodeAndCoreSupport(ProfileItem item)
{
if (item.ConfigType == EConfigType.Custom)
{
return [];
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
return await ValidateNodeAndCoreSupport(item, coreType);
}
private async Task<List<string>> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType);
if (item.ConfigType is EConfigType.Custom)
{
errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString()));
return errors;
}
else if (item.ConfigType.IsGroupType())
{
var groupErrors = await ValidateGroupNode(item, coreType);
errors.AddRange(groupErrors);
return errors;
}
else if (!item.IsComplex())
{
var normalErrors = await ValidateNormalNode(item, coreType);
errors.AddRange(normalErrors);
return errors;
}
return errors;
}
private async Task<List<string>> ValidateNormalNode(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
if (item.Address.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Address"));
return errors;
}
if (item.Port is <= 0 or > 65535)
{
errors.Add(string.Format(ResUI.InvalidProperty, "Port"));
return errors;
}
var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
var transportError = ValidateSingboxTransport(item.ConfigType, net);
if (transportError != null)
{
errors.Add(transportError);
}
if (!Global.SingboxSupportConfigType.Contains(item.ConfigType))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol,
nameof(ECoreType.sing_box), item.ConfigType.ToString()));
}
}
else if (coreType is ECoreType.Xray)
{
// Xray core does not support these protocols
if (!Global.XraySupportConfigType.Contains(item.ConfigType))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol,
nameof(ECoreType.Xray), item.ConfigType.ToString()));
}
}
var protocolExtra = item.GetProtocolExtra();
switch (item.ConfigType)
{
case EConfigType.VMess:
if (item.Password.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Password))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Password"));
}
break;
case EConfigType.VLESS:
if (item.Password.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Password) && item.Password.Length > 30))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Password"));
}
if (!Global.Flows.Contains(protocolExtra.Flow ?? string.Empty))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Flow"));
}
break;
case EConfigType.Shadowsocks:
if (item.Password.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Password"));
}
if (string.IsNullOrEmpty(protocolExtra.SsMethod) || !Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod))
{
errors.Add(string.Format(ResUI.InvalidProperty, "SsMethod"));
}
break;
}
if (item.StreamSecurity == Global.StreamSecurity)
{
// check certificate validity
if (!item.Cert.IsNullOrEmpty()
&& (CertPemManager.ParsePemChain(item.Cert).Count == 0)
&& !item.CertSha.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate"));
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
{
if (item.PublicKey.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey"));
}
}
if (item.Network == nameof(ETransport.xhttp)
&& !item.Extra.IsNullOrEmpty())
{
// check xhttp extra json validity
var xhttpExtra = JsonUtils.ParseJson(item.Extra);
if (xhttpExtra is null)
{
errors.Add(string.Format(ResUI.InvalidProperty, "XHTTP Extra"));
}
}
return errors;
}
private async Task<List<string>> ValidateGroupNode(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
var hasCycle = await GroupProfileManager.HasCycle(item.IndexId, item.GetProtocolExtra());
if (hasCycle)
{
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));
return errors;
}
var (childItems, _) = await GroupProfileManager.GetChildProfileItems(item);
foreach (var childItem in childItems)
{
var childErrors = new List<string>();
if (childItem is null)
{
childErrors.Add(string.Format(ResUI.NodeTagNotExist, ""));
continue;
}
if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain)
{
childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks));
continue;
}
childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType));
errors.AddRange(childErrors.Select(s => s.Insert(0, $"{childItem.Remarks}: ")));
}
return errors;
}
private static string? ValidateSingboxTransport(EConfigType configType, string net)
{
// sing-box does not support xhttp / kcp transports
if (SingboxUnsupportedTransports.Contains(net))
{
return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net);
}
// sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks
if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp))
{
return string.Format(ResUI.CoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
// sing-box shadowsocks only supports tcp/ws/quic transports
if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net))
{
return string.Format(ResUI.CoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
return null;
}
private async Task<List<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item));
errors.AddRange(await ValidateRoutingNodeExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
if (item is null)
{
return errors;
}
// prev node and next node
var subItem = await AppManager.Instance.GetSubItem(item.Subid);
if (subItem is null)
{
return errors;
}
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors);
await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors);
return errors;
}
private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List<string> errors)
{
if (node is not null)
{
var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType);
errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + $"{node.Remarks}: " + s));
}
else if (tag.IsNotEmpty())
{
errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag));
}
}
private async Task<List<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
if (item is null)
{
return errors;
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
var routing = await ConfigHandler.GetDefaultRouting(AppManager.Instance.Config);
if (routing == null)
{
return errors;
}
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var ruleItem in rules ?? [])
{
if (!ruleItem.Enabled)
{
continue;
}
var outboundTag = ruleItem.OutboundTag;
if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag))
{
continue;
}
var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (tagItem is null)
{
errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag));
continue;
}
var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType);
errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + $"{tagItem.Remarks}: " + s));
}
return errors;
}
}

View file

@ -230,6 +230,18 @@ public sealed class AppManager
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.IndexId == indexId); return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.IndexId == indexId);
} }
public async Task<List<ProfileItem>> GetProfileItemsByIndexIds(IEnumerable<string> indexIds)
{
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
}
public async Task<ProfileItem?> GetProfileItemViaRemarks(string? remarks) public async Task<ProfileItem?> GetProfileItemViaRemarks(string? remarks)
{ {
if (remarks.IsNullOrEmpty()) if (remarks.IsNullOrEmpty())

View file

@ -215,7 +215,7 @@ public class CertPemManager
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); await using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
var sslOptions = new SslClientAuthenticationOptions var sslOptions = new SslClientAuthenticationOptions
{ {
@ -262,7 +262,7 @@ public class CertPemManager
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); await using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
var sslOptions = new SslClientAuthenticationOptions var sslOptions = new SslClientAuthenticationOptions
{ {
@ -280,11 +280,7 @@ public class CertPemManager
var chain = new X509Chain(); var chain = new X509Chain();
chain.Build(certChain); chain.Build(certChain);
foreach (var element in chain.ChainElements) pemList.AddRange(chain.ChainElements.Select(element => ExportCertToPem(element.Certificate)));
{
var pem = ExportCertToPem(element.Certificate);
pemList.Add(pem);
}
return (pemList, null); return (pemList, null);
} }

View file

@ -57,16 +57,27 @@ public class CoreManager
} }
} }
public async Task LoadCore(ProfileItem? node) public async Task LoadCore(CoreConfigContext? context)
{ {
if (node == null) if (context == null)
{ {
await UpdateFunc(false, ResUI.CheckServerSettings); await UpdateFunc(false, ResUI.CheckServerSettings);
return; return;
} }
var contextMod = context;
var node = contextMod.Node;
var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName); var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName);
var result = await CoreConfigHandler.GenerateClientConfig(node, fileName); var preContext = ConfigHandler.GetPreSocksCoreConfigContext(contextMod);
if (preContext is not null)
{
contextMod = contextMod with
{
TunProtectSsPort = preContext.TunProtectSsPort,
ProxyRelaySsPort = preContext.ProxyRelaySsPort,
};
}
var result = await CoreConfigHandler.GenerateClientConfig(contextMod, fileName);
if (result.Success != true) if (result.Success != true)
{ {
await UpdateFunc(true, result.Msg); await UpdateFunc(true, result.Msg);
@ -85,8 +96,8 @@ public class CoreManager
await WindowsUtils.RemoveTunDevice(); await WindowsUtils.RemoveTunDevice();
} }
await CoreStart(node); await CoreStart(contextMod);
await CoreStartPreService(node); await CoreStartPreService(preContext);
if (_processService != null) if (_processService != null)
{ {
await UpdateFunc(true, $"{node.GetSummary()}"); await UpdateFunc(true, $"{node.GetSummary()}");
@ -122,7 +133,8 @@ public class CoreManager
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
var configPath = Utils.GetBinConfigPath(fileName); var configPath = Utils.GetBinConfigPath(fileName);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath); var (context, _) = await CoreConfigContextBuilder.Build(_config, node);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, context, testItem, configPath);
if (result.Success != true) if (result.Success != true)
{ {
return null; return null;
@ -165,8 +177,9 @@ public class CoreManager
#region Private #region Private
private async Task CoreStart(ProfileItem node) private async Task CoreStart(CoreConfigContext context)
{ {
var node = context.Node;
var coreType = AppManager.Instance.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.RunningCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
@ -179,27 +192,22 @@ public class CoreManager
_processService = proc; _processService = proc;
} }
private async Task CoreStartPreService(ProfileItem node) private async Task CoreStartPreService(CoreConfigContext? preContext)
{ {
if (_processService != null && !_processService.HasExited) if (_processService is { HasExited: false } && preContext != null)
{ {
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var preCoreType = preContext?.Node?.CoreType ?? ECoreType.sing_box;
var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType); var fileName = Utils.GetBinConfigPath(Global.CorePreConfigFileName);
if (itemSocks != null) var result = await CoreConfigHandler.GenerateClientConfig(preContext, fileName);
if (result.Success)
{ {
var preCoreType = itemSocks.CoreType ?? ECoreType.sing_box; var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
var fileName = Utils.GetBinConfigPath(Global.CorePreConfigFileName); var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
var result = await CoreConfigHandler.GenerateClientConfig(itemSocks, fileName); if (proc is null)
if (result.Success)
{ {
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType); return;
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
if (proc is null)
{
return;
}
_processPreService = proc;
} }
_processPreService = proc;
} }
} }
} }

View file

@ -79,7 +79,7 @@ public class GroupProfileManager
{ {
if (protocolExtra == null) if (protocolExtra == null)
{ {
return new(); return [];
} }
var items = new List<ProfileItem>(); var items = new List<ProfileItem>();
@ -93,27 +93,44 @@ public class GroupProfileManager
{ {
if (extra == null || extra.ChildItems.IsNullOrEmpty()) if (extra == null || extra.ChildItems.IsNullOrEmpty())
{ {
return new(); return [];
} }
var childProfiles = (await Task.WhenAll( var childProfileIds = Utils.String2List(extra.ChildItems)
(Utils.String2List(extra.ChildItems) ?? new()) ?.Where(p => !string.IsNullOrEmpty(p))
.Where(p => !p.IsNullOrEmpty()) .ToList() ?? [];
.Select(AppManager.Instance.GetProfileItem) if (childProfileIds.Count == 0)
)) {
.Where(p => return [];
p != null && }
p.IsValid() &&
p.ConfigType != EConfigType.Custom var childProfiles = await AppManager.Instance.GetProfileItemsByIndexIds(childProfileIds);
) if (childProfiles == null || childProfiles.Count == 0)
.ToList(); {
return childProfiles ?? new(); return [];
}
var profileMap = childProfiles
.Where(p => p != null && !p.IndexId.IsNullOrEmpty())
.GroupBy(p => p!.IndexId!)
.ToDictionary(g => g.Key, g => g.First());
var ordered = new List<ProfileItem>(childProfileIds.Count);
foreach (var id in childProfileIds)
{
if (id != null && profileMap.TryGetValue(id, out var item) && item != null)
{
ordered.Add(item);
}
}
return ordered;
} }
private static async Task<List<ProfileItem>> GetSubChildProfileItems(ProtocolExtraItem? extra) private static async Task<List<ProfileItem>> GetSubChildProfileItems(ProtocolExtraItem? extra)
{ {
if (extra == null || extra.SubChildItems.IsNullOrEmpty()) if (extra == null || extra.SubChildItems.IsNullOrEmpty())
{ {
return new(); return [];
} }
var childProfiles = await AppManager.Instance.ProfileItems(extra.SubChildItems ?? string.Empty); var childProfiles = await AppManager.Instance.ProfileItems(extra.SubChildItems ?? string.Empty);
@ -123,59 +140,31 @@ public class GroupProfileManager
!p.ConfigType.IsComplexType() && !p.ConfigType.IsComplexType() &&
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter)) (extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
) )
.ToList() ?? new(); .ToList() ?? [];
} }
public static async Task<HashSet<string>> GetAllChildDomainAddresses(ProfileItem profileItem) public static async Task<Dictionary<string, ProfileItem>> GetAllChildProfileItems(ProfileItem profileItem)
{ {
var childAddresses = new HashSet<string>(); var itemMap = new Dictionary<string, ProfileItem>();
var (childItems, _) = await GetChildProfileItems(profileItem); var visited = new HashSet<string>();
foreach (var child in childItems)
{ await CollectChildItems(profileItem, itemMap, visited);
if (!child.IsComplex())
{ return itemMap;
childAddresses.Add(child.Address);
}
else if (child.ConfigType.IsGroupType())
{
var subAddresses = await GetAllChildDomainAddresses(child);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
}
}
return childAddresses;
} }
public static async Task<HashSet<string>> GetAllChildEchQuerySni(ProfileItem profileItem) private static async Task CollectChildItems(ProfileItem profileItem, Dictionary<string, ProfileItem> itemMap,
HashSet<string> visited)
{ {
var childAddresses = new HashSet<string>();
var (childItems, _) = await GetChildProfileItems(profileItem); var (childItems, _) = await GetChildProfileItems(profileItem);
foreach (var childNode in childItems) foreach (var child in childItems.Where(child => visited.Add(child.IndexId)))
{ {
if (!childNode.IsComplex() && !childNode.EchConfigList.IsNullOrEmpty()) itemMap[child.IndexId] = child;
if (child.ConfigType.IsGroupType())
{ {
if (childNode.StreamSecurity == Global.StreamSecurity await CollectChildItems(child, itemMap, visited);
&& childNode.EchConfigList?.Contains("://") == true)
{
var idx = childNode.EchConfigList.IndexOf('+');
childAddresses.Add(idx > 0 ? childNode.EchConfigList[..idx] : childNode.Sni);
}
else
{
childAddresses.Add(childNode.Sni);
}
}
else if (childNode.ConfigType.IsGroupType())
{
var subAddresses = await GetAllChildDomainAddresses(childNode);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
} }
} }
return childAddresses;
} }
} }

View file

@ -144,7 +144,6 @@ public class TunModeItem
public bool StrictRoute { get; set; } = true; public bool StrictRoute { get; set; } = true;
public string Stack { get; set; } public string Stack { get; set; }
public int Mtu { get; set; } public int Mtu { get; set; }
public bool EnableExInbound { get; set; }
public bool EnableIPv6Address { get; set; } public bool EnableIPv6Address { get; set; }
} }

View file

@ -0,0 +1,25 @@
namespace ServiceLib.Models;
public record CoreConfigContext
{
public required ProfileItem Node { get; init; }
public required ECoreType RunCoreType { get; init; }
public RoutingItem? RoutingItem { get; init; }
public DNSItem? RawDnsItem { get; init; }
public SimpleDNSItem SimpleDnsItem { get; init; } = new();
public Dictionary<string, ProfileItem> AllProxiesMap { get; init; } = new();
public Config AppConfig { get; init; } = new();
public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new();
// Test ServerTestItem Map
public Dictionary<string, string> ServerTestItemMap { get; init; } = new();
// TUN Compatibility
public bool IsTunEnabled { get; init; } = false;
public HashSet<string> ProtectDomainList { get; init; } = new();
// -> tun inbound --(if routing proxy)--> relay outbound
// -> proxy core (relay inbound --> proxy outbound --(dialerProxy)--> protect outbound)
// -> protect inbound -> direct proxy outbound data -> internet
public int TunProtectSsPort { get; init; } = 0;
public int ProxyRelaySsPort { get; init; } = 0;
}

View file

@ -178,6 +178,7 @@ public class ProfileItem
public string CertSha { get; set; } public string CertSha { get; set; }
public string EchConfigList { get; set; } public string EchConfigList { get; set; }
public string EchForceQuery { get; set; } public string EchForceQuery { get; set; }
public string Finalmask { get; set; }
public string ProtoExtra { get; set; } public string ProtoExtra { get; set; }

View file

@ -255,7 +255,7 @@ public class Server4Sbox : BaseServer4Sbox
// public List<string>? path { get; set; } // hosts // public List<string>? path { get; set; } // hosts
public Dictionary<string, List<string>>? predefined { get; set; } public Dictionary<string, List<string>>? predefined { get; set; }
// Deprecated // Deprecated in sing-box 1.12.0 , kept for backward compatibility
public string? address { get; set; } public string? address { get; set; }
public string? address_resolver { get; set; } public string? address_resolver { get; set; }

View file

@ -341,7 +341,7 @@ public class StreamSettings4Ray
public HysteriaSettings4Ray? hysteriaSettings { get; set; } public HysteriaSettings4Ray? hysteriaSettings { get; set; }
public FinalMask4Ray? finalmask { get; set; } public Finalmask4Ray? finalmask { get; set; }
public Sockopt4Ray? sockopt { get; set; } public Sockopt4Ray? sockopt { get; set; }
} }
@ -472,11 +472,11 @@ public class HysteriaSettings4Ray
public class HysteriaUdpHop4Ray public class HysteriaUdpHop4Ray
{ {
public string? ports { get; set; } public string? port { get; set; }
public string? interval { get; set; } public string? interval { get; set; }
} }
public class FinalMask4Ray public class Finalmask4Ray
{ {
public List<Mask4Ray>? tcp { get; set; } public List<Mask4Ray>? tcp { get; set; }
public List<Mask4Ray>? udp { get; set; } public List<Mask4Ray>? udp { get; set; }
@ -485,7 +485,7 @@ public class FinalMask4Ray
public class Mask4Ray public class Mask4Ray
{ {
public string type { get; set; } public string type { get; set; }
public MaskSettings4Ray? settings { get; set; } public object? settings { get; set; }
} }
public class MaskSettings4Ray public class MaskSettings4Ray

View file

@ -132,33 +132,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support network type &apos;{1}&apos;. 的本地化字符串。
/// </summary>
public static string CoreNotSupportNetwork {
get {
return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos;. 的本地化字符串。
/// </summary>
public static string CoreNotSupportProtocol {
get {
return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos; when using transport &apos;{2}&apos;. 的本地化字符串。
/// </summary>
public static string CoreNotSupportProtocolTransport {
get {
return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。 /// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。
/// </summary> /// </summary>
@ -312,24 +285,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Group &apos;{0}&apos; is empty. Please add at least one node. 的本地化字符串。
/// </summary>
public static string GroupEmpty {
get {
return ResourceManager.GetString("GroupEmpty", resourceCulture);
}
}
/// <summary>
/// 查找类似 {0} Group cannot reference itself or have a circular reference 的本地化字符串。
/// </summary>
public static string GroupSelfReference {
get {
return ResourceManager.GetString("GroupSelfReference", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// 查找类似 This is not the correct configuration, please check 的本地化字符串。
/// </summary> /// </summary>
@ -357,15 +312,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 The {0} property is invalid, please check. 的本地化字符串。
/// </summary>
public static string InvalidProperty {
get {
return ResourceManager.GetString("InvalidProperty", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Invalid address (URL) 的本地化字符串。 /// 查找类似 Invalid address (URL) 的本地化字符串。
/// </summary> /// </summary>
@ -1914,6 +1860,33 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support network type &apos;{1}&apos; 的本地化字符串。
/// </summary>
public static string MsgCoreNotSupportNetwork {
get {
return ResourceManager.GetString("MsgCoreNotSupportNetwork", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos; 的本地化字符串。
/// </summary>
public static string MsgCoreNotSupportProtocol {
get {
return ResourceManager.GetString("MsgCoreNotSupportProtocol", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos; when using transport &apos;{2}&apos; 的本地化字符串。
/// </summary>
public static string MsgCoreNotSupportProtocolTransport {
get {
return ResourceManager.GetString("MsgCoreNotSupportProtocolTransport", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。 /// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
/// </summary> /// </summary>
@ -1959,6 +1932,60 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Group {0} child group node {1} error: {2}. Skipping this node. 的本地化字符串。
/// </summary>
public static string MsgGroupChildGroupNodeError {
get {
return ResourceManager.GetString("MsgGroupChildGroupNodeError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} child group node {1} warning: {2} 的本地化字符串。
/// </summary>
public static string MsgGroupChildGroupNodeWarning {
get {
return ResourceManager.GetString("MsgGroupChildGroupNodeWarning", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} child node {1} error: {2}. Skipping this node. 的本地化字符串。
/// </summary>
public static string MsgGroupChildNodeError {
get {
return ResourceManager.GetString("MsgGroupChildNodeError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} child node {1} warning: {2} 的本地化字符串。
/// </summary>
public static string MsgGroupChildNodeWarning {
get {
return ResourceManager.GetString("MsgGroupChildNodeWarning", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} has a cycle dependency on child node {1}. Skipping this node. 的本地化字符串。
/// </summary>
public static string MsgGroupCycleDependency {
get {
return ResourceManager.GetString("MsgGroupCycleDependency", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} has no valid child node. 的本地化字符串。
/// </summary>
public static string MsgGroupNoValidChildNode {
get {
return ResourceManager.GetString("MsgGroupNoValidChildNode", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Information 的本地化字符串。 /// 查找类似 Information 的本地化字符串。
/// </summary> /// </summary>
@ -1968,6 +1995,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary>
public static string MsgInvalidProperty {
get {
return ResourceManager.GetString("MsgInvalidProperty", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Please enter the URL 的本地化字符串。 /// 查找类似 Please enter the URL 的本地化字符串。
/// </summary> /// </summary>
@ -1977,6 +2013,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Not support protocol &apos;{0}&apos; 的本地化字符串。
/// </summary>
public static string MsgNotSupportProtocol {
get {
return ResourceManager.GetString("MsgNotSupportProtocol", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 No valid subscriptions set 的本地化字符串。 /// 查找类似 No valid subscriptions set 的本地化字符串。
/// </summary> /// </summary>
@ -1995,6 +2040,42 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Routing rule {0} has an empty outbound tag. Fallback to proxy node only. 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleEmptyOutboundTag {
get {
return ResourceManager.GetString("MsgRoutingRuleEmptyOutboundTag", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleOutboundNodeError {
get {
return ResourceManager.GetString("MsgRoutingRuleOutboundNodeError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule {0} outbound node {1} not found. Fallback to proxy node only. 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleOutboundNodeNotFound {
get {
return ResourceManager.GetString("MsgRoutingRuleOutboundNodeNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule {0} outbound node {1} warning: {2} 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleOutboundNodeWarning {
get {
return ResourceManager.GetString("MsgRoutingRuleOutboundNodeWarning", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Filter, press Enter to execute 的本地化字符串。 /// 查找类似 Filter, press Enter to execute 的本地化字符串。
/// </summary> /// </summary>
@ -2049,6 +2130,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Subscription next proxy {0} not found. Skipping. 的本地化字符串。
/// </summary>
public static string MsgSubscriptionNextProfileNotFound {
get {
return ResourceManager.GetString("MsgSubscriptionNextProfileNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 Subscription previous proxy {0} not found. Skipping. 的本地化字符串。
/// </summary>
public static string MsgSubscriptionPrevProfileNotFound {
get {
return ResourceManager.GetString("MsgSubscriptionPrevProfileNotFound", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Unpacking... 的本地化字符串。 /// 查找类似 Unpacking... 的本地化字符串。
/// </summary> /// </summary>
@ -2103,15 +2202,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Node alias &apos;{0}&apos; does not exist. 的本地化字符串。
/// </summary>
public static string NodeTagNotExist {
get {
return ResourceManager.GetString("NodeTagNotExist", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// 查找类似 Non-VMess or SS protocol 的本地化字符串。
/// </summary> /// </summary>
@ -2139,15 +2229,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Not support protocol &apos;{0}&apos;. 的本地化字符串。
/// </summary>
public static string NotSupportProtocol {
get {
return ResourceManager.GetString("NotSupportProtocol", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Scan completed, no valid QR code found 的本地化字符串。 /// 查找类似 Scan completed, no valid QR code found 的本地化字符串。
/// </summary> /// </summary>
@ -2229,24 +2310,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Policy group: 的本地化字符串。
/// </summary>
public static string PolicyGroupPrefix {
get {
return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture);
}
}
/// <summary>
/// 查找类似 Proxy chained: 的本地化字符串。
/// </summary>
public static string ProxyChainedPrefix {
get {
return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。 /// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。
/// </summary> /// </summary>
@ -2310,15 +2373,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Routing rule outbound: 的本地化字符串。
/// </summary>
public static string RoutingRuleOutboundPrefix {
get {
return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Run as Admin 的本地化字符串。 /// 查找类似 Run as Admin 的本地化字符串。
/// </summary> /// </summary>
@ -2916,6 +2970,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Finalmask 的本地化字符串。
/// </summary>
public static string TbFinalmask {
get {
return ResourceManager.GetString("TbFinalmask", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Fingerprint 的本地化字符串。 /// 查找类似 Fingerprint 的本地化字符串。
/// </summary> /// </summary>
@ -3771,15 +3834,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Enable additional Inbound 的本地化字符串。
/// </summary>
public static string TbSettingsEnableExInbound {
get {
return ResourceManager.GetString("TbSettingsEnableExInbound", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Enable fragment 的本地化字符串。 /// 查找类似 Enable fragment 的本地化字符串。
/// </summary> /// </summary>
@ -3789,15 +3843,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 which conflicts with the group previous proxy 的本地化字符串。
/// </summary>
public static string TbSettingsEnableFragmentTips {
get {
return ResourceManager.GetString("TbSettingsEnableFragmentTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Enable hardware acceleration (requires restart) 的本地化字符串。 /// 查找类似 Enable hardware acceleration (requires restart) 的本地化字符串。
/// </summary> /// </summary>

View file

@ -1071,9 +1071,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>فعال سازی additional Inbound</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>فعال سازی آدرس IPv6</value> <value>فعال سازی آدرس IPv6</value>
</data> </data>
@ -1113,9 +1110,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>افزودن سرور [HTTP]</value> <value>افزودن سرور [HTTP]</value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>which conflicts with the group previous proxy</value>
</data>
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>فعال کردن فرگمنت</value> <value>فعال کردن فرگمنت</value>
</data> </data>
@ -1545,38 +1539,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value> <value>Multi-Configuration Fallback by Xray</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value> <value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>Proxy chained: </value> <value>The {0} property is invalid, please check</value>
</data> </data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Routing rule outbound: </value> <value>Not support protocol '{0}'</value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value> <value>If the system does not have a tray function, please do not enable it</value>
@ -1671,4 +1647,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value> <value>Configuration item preview</value>
</data> </data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
</root> </root>

View file

@ -1068,9 +1068,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>Activer un port découte supplémentaire</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>Activer IPv6</value> <value>Activer IPv6</value>
</data> </data>
@ -1110,9 +1107,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>Ajouter [HTTP]</value> <value>Ajouter [HTTP]</value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>En conflit avec le proxy amont de groupe</value>
</data>
<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>
@ -1542,38 +1536,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Xray basculement (multi-sélection)</value> <value>Xray basculement (multi-sélection)</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le type de réseau « {1} ».</value> <value>Le cœur « {0} » ne prend pas en charge le type de réseau « {1} »</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} ».</value> <value>Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} »</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le protocole « {1} ».</value> <value>Le cœur « {0} » ne prend pas en charge le protocole « {1} »</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>Chaîne de proxy : </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Règle de routage sortante : </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Groupe de stratégie : </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Lalias « {0} » nexiste pas.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Le groupe « {0} » est vide. Veuillez ajouter au moins une configuration.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>La propriété {0} est invalide, veuillez vérifier</value> <value>La propriété {0} est invalide, veuillez vérifier</value>
</data> </data>
<data name="GroupSelfReference" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Le groupe {0} ne peut pas se référencer lui-même ni créer de référence circulaire</value> <value>Protocole « {0} » non pris en charge</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Protocole « {0} » non pris en charge.</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>Si le système na pas de zone de notif., nactivez pas cette option</value> <value>Si le système na pas de zone de notif., nactivez pas cette option</value>
@ -1639,33 +1615,72 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>EchForceQuery</value> <value>EchForceQuery</value>
</data> </data>
<data name="TbFullCertTips" xml:space="preserve"> <data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value> <value>Certificat complet (chaîne), format PEM</value>
</data> </data>
<data name="TbCertSha256Tips" xml:space="preserve"> <data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value> <value>Empreinte du certificat (SHA-256)</value>
</data> </data>
<data name="TbServeStale" xml:space="preserve"> <data name="TbServeStale" xml:space="preserve">
<value>Serve Stale</value> <value>Cache optimiste</value>
</data> </data>
<data name="TbParallelQuery" xml:space="preserve"> <data name="TbParallelQuery" xml:space="preserve">
<value>Parallel Query</value> <value>Requête parallèle</value>
</data> </data>
<data name="TbDomesticDNSTips" xml:space="preserve"> <data name="TbDomesticDNSTips" xml:space="preserve">
<value>By default, invoked only during routing for resolution</value> <value>Par défaut, utilisé uniquement lors du routage pour la résolution.</value>
</data> </data>
<data name="TbRemoteDNSTips" xml:space="preserve"> <data name="TbRemoteDNSTips" xml:space="preserve">
<value>By default, invoked only during routing for resolution; ensure the remote server can reach this DNS</value> <value>Par défaut, invoqué uniquement au routage pour la résolution. Vérifiez que le serveur distant peut joindre ce DNS.</value>
</data> </data>
<data name="TbDirectResolveStrategyTips" xml:space="preserve"> <data name="TbDirectResolveStrategyTips" xml:space="preserve">
<value>If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used.</value> <value>Si non défini ou « AsIs », le DNS système est utilisé ; sinon, le module DNS interne est utilisé.</value>
</data> </data>
<data name="TbRemoteResolveStrategyTips" xml:space="preserve"> <data name="TbRemoteResolveStrategyTips" xml:space="preserve">
<value>If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used.</value> <value>Si non défini ou « AsIs », la résolution DNS est assurée par le serveur distant ; sinon, le module DNS interne est utilisé.</value>
</data> </data>
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Intervalle de saut de port</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value> <value>Aperçu des sous-config</value>
</data> </data>
</root> <data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
</root>

View file

@ -1071,9 +1071,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>További bejövő engedélyezése</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>IPv6 cím engedélyezése</value> <value>IPv6 cím engedélyezése</value>
</data> </data>
@ -1113,9 +1110,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>HTTP konfiguráció hozzáadása</value> <value>HTTP konfiguráció hozzáadása</value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>which conflicts with the group previous proxy</value>
</data>
<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>
@ -1545,38 +1539,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value> <value>Multi-Configuration Fallback by Xray</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value> <value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>Proxy chained: </value> <value>The {0} property is invalid, please check</value>
</data> </data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Routing rule outbound: </value> <value>Not support protocol '{0}'</value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value> <value>If the system does not have a tray function, please do not enable it</value>
@ -1671,4 +1647,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value> <value>Configuration item preview</value>
</data> </data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
</root> </root>

View file

@ -1071,9 +1071,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>Enable additional Inbound</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>Enable IPv6 Address</value> <value>Enable IPv6 Address</value>
</data> </data>
@ -1113,9 +1110,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>Add [HTTP]</value> <value>Add [HTTP]</value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>which conflicts with the group previous proxy</value>
</data>
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Enable fragment</value> <value>Enable fragment</value>
</data> </data>
@ -1545,38 +1539,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Fallback by Xray</value> <value>Fallback by Xray</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value> <value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>Proxy chained: </value> <value>The {0} property is invalid, please check</value>
</data> </data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Routing rule outbound: </value> <value>Not support protocol '{0}'</value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value> <value>If the system does not have a tray function, please do not enable it</value>
@ -1671,4 +1647,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value> <value>Configuration item preview</value>
</data> </data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
</root> </root>

View file

@ -1071,9 +1071,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>Включить дополнительный входящий канал</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>Включить IPv6 адреса</value> <value>Включить IPv6 адреса</value>
</data> </data>
@ -1113,9 +1110,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>Добавить сервер [HTTP]</value> <value>Добавить сервер [HTTP]</value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>что конфликтует с предыдущим прокси группы</value>
</data>
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>Включить фрагментацию (Fragment)</value> <value>Включить фрагментацию (Fragment)</value>
</data> </data>
@ -1545,38 +1539,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value> <value>Multi-Configuration Fallback by Xray</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value> <value>Core '{0}' does not support network type '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value> <value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value> <value>Core '{0}' does not support protocol '{1}'</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>Proxy chained: </value> <value>The {0} property is invalid, please check</value>
</data> </data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Routing rule outbound: </value> <value>Not support protocol '{0}'</value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value> <value>If the system does not have a tray function, please do not enable it</value>
@ -1671,4 +1647,43 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value> <value>Configuration item preview</value>
</data> </data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
</root> </root>

View file

@ -1068,9 +1068,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>启用额外监听端口</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>启用 IPv6</value> <value>启用 IPv6</value>
</data> </data>
@ -1110,9 +1107,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>添加 [HTTP] </value> <value>添加 [HTTP] </value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>和分组前置代理冲突</value>
</data>
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>启用分片 (Fragment)</value> <value>启用分片 (Fragment)</value>
</data> </data>
@ -1542,38 +1536,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>多选故障转移 Xray</value> <value>多选故障转移 Xray</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支持网络类型 '{1}'</value> <value>核心 '{0}' 不支持网络类型 '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'</value> <value>核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支持协议 '{1}'</value> <value>核心 '{0}' 不支持协议 '{1}'</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>代理链: </value> <value>{0} 属性无效,请检查</value>
</data> </data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>路由规则出站: </value> <value>不支持协议 '{0}'</value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>策略组: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>别名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>组“{0}”为空。请至少添加一个配置。</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>{0}属性无效,请检查</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分组不能引用自身或循环引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>不支持协议 '{0}'。</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系统没有托盘功能,请不要开启</value> <value>如果系统没有托盘功能,请不要开启</value>
@ -1668,4 +1644,43 @@
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>子配置项预览</value> <value>子配置项预览</value>
</data> </data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>路由规则 {0} 出站节点 {1} 警告:{2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>路由规则 {0} 出站节点 {1} 错误:{2}。已回退为仅使用代理节点。</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>节点组 {0} 与子节点 {1} 存在循环依赖,已跳过该节点。</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>节点组 {0} 子节点 {1} 警告:{2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>节点组 {0} 子节点 {1} 错误:{2}。已跳过该节点。</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>节点组 {0} 子节点组 {1} 警告:{2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>节点组 {0} 子节点组 {1} 错误:{2}。已跳过该节点。</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>节点组 {0} 下没有有效的子节点。</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>路由规则 {0} 的出站标签为空,已回退为仅使用代理节点。</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>路由规则 {0} 的出站节点 {1} 未找到,已回退为仅使用代理节点。</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>订阅前置节点 {0} 未找到,已跳过。</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>订阅后置节点 {0} 未找到,已跳过。</value>
</data>
</root> </root>

View file

@ -1068,9 +1068,6 @@
<data name="TbSettingsTunMtu" xml:space="preserve"> <data name="TbSettingsTunMtu" xml:space="preserve">
<value>MTU</value> <value>MTU</value>
</data> </data>
<data name="TbSettingsEnableExInbound" xml:space="preserve">
<value>啟用額外偵聽連接埠</value>
</data>
<data name="TbSettingsEnableIPv6Address" xml:space="preserve"> <data name="TbSettingsEnableIPv6Address" xml:space="preserve">
<value>啟用 IPv6</value> <value>啟用 IPv6</value>
</data> </data>
@ -1110,9 +1107,6 @@
<data name="menuAddHttpServer" xml:space="preserve"> <data name="menuAddHttpServer" xml:space="preserve">
<value>新增 [HTTP] 節點</value> <value>新增 [HTTP] 節點</value>
</data> </data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>和分組前置代理衝突</value>
</data>
<data name="TbSettingsEnableFragment" xml:space="preserve"> <data name="TbSettingsEnableFragment" xml:space="preserve">
<value>啟用分片Fragment</value> <value>啟用分片Fragment</value>
</data> </data>
@ -1542,38 +1536,20 @@
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve"> <data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>多選容錯移轉 Xray</value> <value>多選容錯移轉 Xray</value>
</data> </data>
<data name="CoreNotSupportNetwork" xml:space="preserve"> <data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支援網路類型 '{1}'.</value> <value>核心 '{0}' 不支援網路類型 '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve"> <data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'.</value> <value>核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'</value>
</data> </data>
<data name="CoreNotSupportProtocol" xml:space="preserve"> <data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支援協定 '{1}'.</value> <value>核心 '{0}' 不支援協定 '{1}'</value>
</data> </data>
<data name="ProxyChainedPrefix" xml:space="preserve"> <data name="MsgInvalidProperty" xml:space="preserve">
<value>代理鏈: </value> <value>{0} 屬性無效,請檢查</value>
</data> </data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve"> <data name="MsgNotSupportProtocol" xml:space="preserve">
<value>路由規則出站: </value> <value>不支援協定 '{0}'</value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>策略組: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>別名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>組“{0}”為空.請至少添加一個配置。</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>{0}屬性無效,請檢查</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分組不能引用自身或循環引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>不支援協定 '{0}'.</value>
</data> </data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve"> <data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系統沒有託盤功能,請不要開啟</value> <value>如果系統沒有託盤功能,請不要開啟</value>
@ -1668,4 +1644,43 @@
<data name="menuServerListPreview" xml:space="preserve"> <data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value> <value>Configuration item preview</value>
</data> </data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
</root> </root>

View file

@ -1,4 +1,4 @@
{ {
"log": { "log": {
"access": "Vaccess.log", "access": "Vaccess.log",
"error": "Verror.log", "error": "Verror.log",
@ -6,34 +6,6 @@
}, },
"inbounds": [], "inbounds": [],
"outbounds": [ "outbounds": [
{
"tag": "proxy",
"protocol": "vmess",
"settings": {
"vnext": [{
"address": "",
"port": 0,
"users": [{
"id": "",
"security": "auto"
}]
}],
"servers": [{
"address": "",
"method": "",
"ota": false,
"password": "",
"port": 0,
"level": 1
}]
},
"streamSettings": {
"network": "tcp"
},
"mux": {
"enabled": false
}
},
{ {
"protocol": "freedom", "protocol": "freedom",
"tag": "direct" "tag": "direct"

View file

@ -5,19 +5,12 @@
}, },
"inbounds": [], "inbounds": [],
"outbounds": [ "outbounds": [
{
"type": "vless",
"tag": "proxy",
"server": "",
"server_port": 443
},
{ {
"type": "direct", "type": "direct",
"tag": "direct" "tag": "direct"
} }
], ],
"route": { "route": {
"rules": [ "rules": []
]
} }
} }

View file

@ -1,43 +1,34 @@
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService(Config config) public partial class CoreConfigSingboxService(CoreConfigContext context)
{ {
private readonly Config _config = config;
private static readonly string _tag = "CoreConfigSingboxService"; private static readonly string _tag = "CoreConfigSingboxService";
private readonly Config _config = context.AppConfig;
private readonly ProfileItem _node = context.Node;
private SingboxConfig _coreConfig = new();
#region public gen function #region public gen function
public async Task<RetResult> GenerateClientConfigContent(ProfileItem node) public RetResult GenerateClientConfigContent()
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (node == null if (_node == null
|| !node.IsValid()) || !_node.IsValid())
{ {
ret.Msg = ResUI.CheckServerSettings; ret.Msg = ResUI.CheckServerSettings;
return ret; return ret;
} }
if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) if (_node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
{ {
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}";
return ret; return ret;
} }
ret.Msg = ResUI.InitialConfiguration; ret.Msg = ResUI.InitialConfiguration;
if (node.ConfigType.IsGroupType())
{
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
return await GenerateClientMultipleLoadConfig(node);
case EConfigType.ProxyChain:
return await GenerateClientChainConfig(node);
}
}
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
@ -45,44 +36,76 @@ public partial class CoreConfigSingboxService(Config config)
return ret; return ret;
} }
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result); _coreConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null) if (_coreConfig == null)
{ {
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenLog(singboxConfig); GenLog();
await GenInbounds(singboxConfig); GenInbounds();
if (node.ConfigType == EConfigType.WireGuard) GenOutbounds();
{
singboxConfig.outbounds.RemoveAt(0);
var endpoints = new Endpoints4Sbox();
await GenEndpoint(node, endpoints);
endpoints.tag = Global.ProxyTag;
singboxConfig.endpoints = new() { endpoints };
}
else
{
await GenOutbound(node, singboxConfig.outbounds.First());
}
await GenMoreOutbounds(node, singboxConfig); GenRouting();
await GenRouting(singboxConfig); GenDns();
await GenDns(node, singboxConfig); GenExperimental();
await GenExperimental(singboxConfig); ConvertGeo2Ruleset();
await ConvertGeo2Ruleset(singboxConfig);
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(singboxConfig); ret.Data = ApplyFullConfigTemplate();
if (context.TunProtectSsPort is > 0 and <= 65535)
{
var ssInbound = new
{
type = "shadowsocks",
tag = "tun-protect-ss",
listen = Global.Loopback,
listen_port = context.TunProtectSsPort,
method = "none",
password = "none",
};
var directRule = new Rule4Sbox()
{
inbound = new List<string> { ssInbound.tag },
outbound = Global.DirectTag,
};
var singboxConfigNode = JsonUtils.ParseJson(ret.Data.ToString())!.AsObject();
var inboundsNode = singboxConfigNode["inbounds"]!.AsArray();
inboundsNode.Add(JsonUtils.SerializeToNode(ssInbound, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}));
var routeNode = singboxConfigNode["route"]?.AsObject();
var rulesNode = routeNode?["rules"]?.AsArray();
var protectRuleNode = JsonUtils.SerializeToNode(directRule,
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
if (rulesNode != null)
{
rulesNode.Insert(0, protectRuleNode);
}
else
{
var newRulesNode = new JsonArray() { protectRuleNode };
if (routeNode is null)
{
var newRouteNode = new JsonObject() { ["rules"] = newRulesNode };
singboxConfigNode["route"] = newRouteNode;
}
else
{
routeNode["rules"] = newRulesNode;
}
}
ret.Data = JsonUtils.Serialize(singboxConfigNode);
}
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -93,17 +116,11 @@ public partial class CoreConfigSingboxService(Config config)
} }
} }
public async Task<RetResult> GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds) public RetResult GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration; ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
@ -114,29 +131,19 @@ public partial class CoreConfigSingboxService(Config config)
return ret; return ret;
} }
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result); _coreConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null) if (_coreConfig == null)
{ {
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
List<IPEndPoint> lstIpEndPoints = new();
List<TcpConnectionInformation> lstTcpConns = new();
try
{
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await GenLog(singboxConfig); var (lstIpEndPoints, lstTcpConns) = Utils.GetActiveNetworkInfo();
//GenDns(new(), singboxConfig);
singboxConfig.inbounds.Clear(); GenLog();
singboxConfig.outbounds.RemoveAt(0); GenMinimizedDns();
_coreConfig.inbounds.Clear();
_coreConfig.outbounds.RemoveAt(0);
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest); var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
@ -150,8 +157,9 @@ public partial class CoreConfigSingboxService(Config config)
{ {
continue; continue;
} }
var item = await AppManager.Instance.GetProfileItem(it.IndexId); var actIndexId = context.ServerTestItemMap.GetValueOrDefault(it.IndexId, it.IndexId);
if (item is null || item.IsComplex() || !item.IsValid()) var item = context.AllProxiesMap.GetValueOrDefault(actIndexId);
if (item is null || item.ConfigType is EConfigType.Custom || !item.IsValid())
{ {
continue; continue;
} }
@ -190,26 +198,11 @@ public partial class CoreConfigSingboxService(Config config)
type = EInboundProtocol.mixed.ToString(), type = EInboundProtocol.mixed.ToString(),
}; };
inbound.tag = inbound.type + inbound.listen_port.ToString(); inbound.tag = inbound.type + inbound.listen_port.ToString();
singboxConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
//outbound
var server = await GenServer(item);
if (server is null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
var tag = Global.ProxyTag + inbound.listen_port.ToString(); var tag = Global.ProxyTag + inbound.listen_port.ToString();
server.tag = tag; var serverList = new CoreConfigSingboxService(context with { Node = item }).BuildAllProxyOutbounds(tag);
if (server is Endpoints4Sbox endpoint) FillRangeProxy(serverList, _coreConfig, false);
{
singboxConfig.endpoints ??= new();
singboxConfig.endpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
singboxConfig.outbounds.Add(outbound);
}
//rule //rule
Rule4Sbox rule = new() Rule4Sbox rule = new()
@ -217,25 +210,11 @@ public partial class CoreConfigSingboxService(Config config)
inbound = new List<string> { inbound.tag }, inbound = new List<string> { inbound.tag },
outbound = tag outbound = tag
}; };
singboxConfig.route.rules.Add(rule); _coreConfig.route.rules.Add(rule);
} }
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (rawDNSItem != null && rawDNSItem.Enabled == true)
{
await GenDnsDomainsCompatible(singboxConfig, rawDNSItem);
}
else
{
await GenDnsDomains(singboxConfig, _config.SimpleDNSItem);
}
singboxConfig.route.default_domain_resolver = new()
{
server = Global.SingboxLocalDNSTag,
};
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig); ret.Data = JsonUtils.Serialize(_coreConfig);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -246,20 +225,20 @@ public partial class CoreConfigSingboxService(Config config)
} }
} }
public async Task<RetResult> GenerateClientSpeedtestConfig(ProfileItem node, int port) public RetResult GenerateClientSpeedtestConfig(int port)
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (node == null if (_node == null
|| !node.IsValid()) || !_node.IsValid())
{ {
ret.Msg = ResUI.CheckServerSettings; ret.Msg = ResUI.CheckServerSettings;
return ret; return ret;
} }
if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) if (_node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
{ {
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}";
return ret; return ret;
} }
@ -272,44 +251,20 @@ public partial class CoreConfigSingboxService(Config config)
return ret; return ret;
} }
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result); _coreConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null) if (_coreConfig == null)
{ {
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenLog(singboxConfig); GenLog();
if (node.ConfigType == EConfigType.WireGuard) GenOutbounds();
{ GenMinimizedDns();
singboxConfig.outbounds.RemoveAt(0);
var endpoints = new Endpoints4Sbox();
await GenEndpoint(node, endpoints);
endpoints.tag = Global.ProxyTag;
singboxConfig.endpoints = new() { endpoints };
}
else
{
await GenOutbound(node, singboxConfig.outbounds.First());
}
await GenMoreOutbounds(node, singboxConfig);
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (item != null && item.Enabled == true)
{
await GenDnsDomainsCompatible(singboxConfig, item);
}
else
{
await GenDnsDomains(singboxConfig, _config.SimpleDNSItem);
}
singboxConfig.route.default_domain_resolver = new()
{
server = Global.SingboxLocalDNSTag,
};
singboxConfig.route.rules.Clear(); _coreConfig.route.rules.Clear();
singboxConfig.inbounds.Clear(); _coreConfig.inbounds.Clear();
singboxConfig.inbounds.Add(new() _coreConfig.inbounds.Add(new()
{ {
tag = $"{EInboundProtocol.mixed}{port}", tag = $"{EInboundProtocol.mixed}{port}",
listen = Global.Loopback, listen = Global.Loopback,
@ -319,202 +274,7 @@ public partial class CoreConfigSingboxService(Config config)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig); ret.Data = JsonUtils.Serialize(_coreConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(parentNode, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(parentNode, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientCustomConfig(ProfileItem node, string? fileName)
{
var ret = new RetResult();
if (node == null || fileName is null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
try
{
if (node == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (File.Exists(fileName))
{
File.Delete(fileName);
}
var addressFileName = node.Address;
if (addressFileName.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
if (!File.Exists(addressFileName))
{
addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName);
}
if (!File.Exists(addressFileName))
{
ret.Msg = ResUI.FailedReadConfiguration + "1";
return ret;
}
if (node.Address == Global.CoreMultipleLoadConfigFileName)
{
var txtFile = File.ReadAllText(addressFileName);
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(txtFile);
if (singboxConfig == null)
{
File.Copy(addressFileName, fileName);
}
else
{
await GenInbounds(singboxConfig);
await GenExperimental(singboxConfig);
var content = JsonUtils.Serialize(singboxConfig, true);
await File.WriteAllTextAsync(fileName, content);
}
}
else
{
File.Copy(addressFileName, fileName);
}
//check again
if (!File.Exists(fileName))
{
ret.Msg = ResUI.FailedReadConfiguration + "2";
return ret;
}
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)

View file

@ -2,29 +2,29 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<string> ApplyFullConfigTemplate(SingboxConfig singboxConfig) private string ApplyFullConfigTemplate()
{ {
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); var fullConfigTemplate = context.FullConfigTemplate;
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled) if (fullConfigTemplate is not { Enabled: true })
{ {
return JsonUtils.Serialize(singboxConfig); return JsonUtils.Serialize(_coreConfig);
} }
var fullConfigTemplateItem = _config.TunModeItem.EnableTun ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
if (fullConfigTemplateItem.IsNullOrEmpty()) if (fullConfigTemplateItem.IsNullOrEmpty())
{ {
return JsonUtils.Serialize(singboxConfig); return JsonUtils.Serialize(_coreConfig);
} }
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null) if (fullConfigTemplateNode == null)
{ {
return JsonUtils.Serialize(singboxConfig); return JsonUtils.Serialize(_coreConfig);
} }
// Process outbounds // Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray(); var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : [];
foreach (var outbound in singboxConfig.outbounds) foreach (var outbound in _coreConfig.outbounds)
{ {
if (outbound.type.ToLower() is "direct" or "block") if (outbound.type.ToLower() is "direct" or "block")
{ {
@ -42,10 +42,10 @@ public partial class CoreConfigSingboxService
fullConfigTemplateNode["outbounds"] = customOutboundsNode; fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints // Process endpoints
if (singboxConfig.endpoints != null && singboxConfig.endpoints.Count > 0) if (_coreConfig.endpoints != null && _coreConfig.endpoints.Count > 0)
{ {
var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : new JsonArray(); var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : [];
foreach (var endpoint in singboxConfig.endpoints) foreach (var endpoint in _coreConfig.endpoints)
{ {
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
{ {
@ -56,6 +56,6 @@ public partial class CoreConfigSingboxService
fullConfigTemplateNode["endpoints"] = customEndpointsNode; fullConfigTemplateNode["endpoints"] = customEndpointsNode;
} }
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode)); return JsonUtils.Serialize(fullConfigTemplateNode);
} }
} }

View file

@ -2,65 +2,68 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<int> GenDns(ProfileItem? node, SingboxConfig singboxConfig) private void GenDns()
{ {
try try
{ {
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); var item = context.RawDnsItem;
if (item != null && item.Enabled == true) if (item is { Enabled: true })
{ {
return await GenDnsCompatible(node, singboxConfig); GenDnsCustom();
return;
} }
var simpleDNSItem = _config.SimpleDNSItem; GenDnsServers();
await GenDnsServers(node, singboxConfig, simpleDNSItem); GenDnsRules();
await GenDnsRules(node, singboxConfig, simpleDNSItem);
singboxConfig.dns ??= new Dns4Sbox(); _coreConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.independent_cache = true; _coreConfig.dns.independent_cache = true;
// final dns // final dns
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = context.RoutingItem;
var useDirectDns = false; var useDirectDns = false;
if (routing != null) if (routing != null)
{ {
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? []; var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
useDirectDns = rules?.LastOrDefault() is { } lastRule && if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
lastRule.OutboundTag == Global.DirectTag && {
(lastRule.Port == "0-65535" || var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
lastRule.Network == "tcp,udp" || var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
lastRule.Ip?.Contains("0.0.0.0/0") == true); var isAnyIp = lastRule.Ip == null || lastRule.Ip.Count == 0 || lastRule.Ip.Contains("0.0.0.0/0");
var isAnyPort = string.IsNullOrEmpty(lastRule.Port) || lastRule.Port == "0-65535";
var isAnyNetwork = string.IsNullOrEmpty(lastRule.Network) || lastRule.Network == "tcp,udp";
useDirectDns = noDomain && noProcess && isAnyIp && isAnyPort && isAnyNetwork;
}
} }
singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; _coreConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false) var simpleDnsItem = context.SimpleDnsItem;
if ((!useDirectDns) && simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == false)
{ {
singboxConfig.dns.rules.Add(new() _coreConfig.dns.rules.Add(new()
{ {
server = Global.SingboxFakeDNSTag, server = Global.SingboxFakeDNSTag,
query_type = new List<int> { 1, 28 }, // A and AAAA query_type = new List<int> { 1, 28 }, // A and AAAA
rewrite_ttl = 1, rewrite_ttl = 1,
}); });
} }
await GenOutboundDnsRule(node, singboxConfig);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private async Task<int> GenDnsServers(ProfileItem? node, SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem) private void GenDnsServers()
{ {
var finalDns = await GenDnsDomains(singboxConfig, simpleDNSItem); var simpleDnsItem = context.SimpleDnsItem;
var finalDns = GenBootstrapDns();
var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS); var directDns = ParseDnsAddress(simpleDnsItem.DirectDNS ?? Global.DomainDirectDNSAddress.First());
directDns.tag = Global.SingboxDirectDNSTag; directDns.tag = Global.SingboxDirectDNSTag;
directDns.domain_resolver = Global.SingboxLocalDNSTag; directDns.domain_resolver = Global.SingboxLocalDNSTag;
var remoteDns = ParseDnsAddress(simpleDNSItem.RemoteDNS); var remoteDns = ParseDnsAddress(simpleDnsItem.RemoteDNS ?? Global.DomainRemoteDNSAddress.First());
remoteDns.tag = Global.SingboxRemoteDNSTag; remoteDns.tag = Global.SingboxRemoteDNSTag;
remoteDns.detour = Global.ProxyTag; remoteDns.detour = Global.ProxyTag;
remoteDns.domain_resolver = Global.SingboxLocalDNSTag; remoteDns.domain_resolver = Global.SingboxLocalDNSTag;
@ -71,12 +74,12 @@ public partial class CoreConfigSingboxService
type = "hosts", type = "hosts",
predefined = new(), predefined = new(),
}; };
if (simpleDNSItem.AddCommonHosts == true) if (simpleDnsItem.AddCommonHosts == true)
{ {
hostsDns.predefined = Global.PredefinedHosts; hostsDns.predefined = Global.PredefinedHosts;
} }
if (simpleDNSItem.UseSystemHosts == true) if (simpleDnsItem.UseSystemHosts == true)
{ {
var systemHosts = Utils.GetSystemHosts(); var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0) if (systemHosts != null && systemHosts.Count > 0)
@ -88,9 +91,9 @@ public partial class CoreConfigSingboxService
} }
} }
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts))
{ {
hostsDns.predefined[kvp.Key] = kvp.Value.Where(s => Utils.IsIpAddress(s)).ToList(); hostsDns.predefined[kvp.Key] = kvp.Value.Where(Utils.IsIpAddress).ToList();
} }
foreach (var host in hostsDns.predefined) foreach (var host in hostsDns.predefined)
@ -109,14 +112,14 @@ public partial class CoreConfigSingboxService
} }
} }
singboxConfig.dns ??= new Dns4Sbox(); _coreConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.servers ??= []; _coreConfig.dns.servers ??= [];
singboxConfig.dns.servers.Add(remoteDns); _coreConfig.dns.servers.Add(remoteDns);
singboxConfig.dns.servers.Add(directDns); _coreConfig.dns.servers.Add(directDns);
singboxConfig.dns.servers.Add(hostsDns); _coreConfig.dns.servers.Add(hostsDns);
// fake ip // fake ip
if (simpleDNSItem.FakeIP == true) if (simpleDnsItem.FakeIP == true)
{ {
var fakeip = new Server4Sbox var fakeip = new Server4Sbox
{ {
@ -125,68 +128,50 @@ public partial class CoreConfigSingboxService
inet4_range = "198.18.0.0/15", inet4_range = "198.18.0.0/15",
inet6_range = "fc00::/18", inet6_range = "fc00::/18",
}; };
singboxConfig.dns.servers.Add(fakeip); _coreConfig.dns.servers.Add(fakeip);
} }
// ech
var (_, dnsServer) = ParseEchParam(node?.EchConfigList);
if (dnsServer is not null)
{
dnsServer.tag = Global.SingboxEchDNSTag;
if (dnsServer.server is not null
&& hostsDns.predefined.ContainsKey(dnsServer.server))
{
dnsServer.domain_resolver = Global.SingboxHostsDNSTag;
}
else
{
dnsServer.domain_resolver = Global.SingboxLocalDNSTag;
}
singboxConfig.dns.servers.Add(dnsServer);
}
else if (node?.ConfigType.IsGroupType() == true)
{
var echDnsObject = JsonUtils.DeepCopy(directDns);
echDnsObject.tag = Global.SingboxEchDNSTag;
singboxConfig.dns.servers.Add(echDnsObject);
}
return await Task.FromResult(0);
} }
private async Task<Server4Sbox> GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem) private Server4Sbox GenBootstrapDns()
{ {
var finalDns = ParseDnsAddress(simpleDNSItem.BootstrapDNS); var finalDns = ParseDnsAddress(context.SimpleDnsItem?.BootstrapDNS ?? Global.DomainPureIPDNSAddress.First());
finalDns.tag = Global.SingboxLocalDNSTag; finalDns.tag = Global.SingboxLocalDNSTag;
singboxConfig.dns ??= new Dns4Sbox(); _coreConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.servers ??= new List<Server4Sbox>(); _coreConfig.dns.servers ??= [];
singboxConfig.dns.servers.Add(finalDns); _coreConfig.dns.servers.Add(finalDns);
return await Task.FromResult(finalDns); return finalDns;
} }
private async Task<int> GenDnsRules(ProfileItem? node, SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem) private void GenDnsRules()
{ {
singboxConfig.dns ??= new Dns4Sbox(); var simpleDnsItem = context.SimpleDnsItem;
singboxConfig.dns.rules ??= new List<Rule4Sbox>(); _coreConfig.dns ??= new Dns4Sbox();
_coreConfig.dns.rules ??= [];
singboxConfig.dns.rules.AddRange(new[] _coreConfig.dns.rules.AddRange(new[]
{ {
new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag }, new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag },
new Rule4Sbox new Rule4Sbox
{
server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
domain = context.ProtectDomainList.ToList(),
},
new Rule4Sbox
{ {
server = Global.SingboxRemoteDNSTag, server = Global.SingboxRemoteDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Proxy), strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy),
clash_mode = ERuleMode.Global.ToString() clash_mode = ERuleMode.Global.ToString()
}, },
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 = ERuleMode.Direct.ToString()
} }
}); });
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts))
{ {
var predefined = kvp.Value.First(); var predefined = kvp.Value.First();
if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined)) if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined))
@ -197,7 +182,7 @@ public partial class CoreConfigSingboxService
{ {
// xray syntactic sugar for predefined // xray syntactic sugar for predefined
// etc. #0 -> NOERROR // etc. #0 -> NOERROR
singboxConfig.dns.rules.Add(new() _coreConfig.dns.rules.Add(new()
{ {
query_type = [1, 28], query_type = [1, 28],
domain = [kvp.Key], domain = [kvp.Key],
@ -225,38 +210,13 @@ public partial class CoreConfigSingboxService
}; };
if (ParseV2Domain(kvp.Key, rule)) if (ParseV2Domain(kvp.Key, rule))
{ {
singboxConfig.dns.rules.Add(rule); _coreConfig.dns.rules.Add(rule);
} }
} }
var (ech, _) = ParseEchParam(node?.EchConfigList); if (simpleDnsItem.BlockBindingQuery == true)
if (ech is not null)
{ {
var echDomain = ech.query_server_name ?? node?.Sni; _coreConfig.dns.rules.Add(new()
singboxConfig.dns.rules.Add(new()
{
query_type = [64, 65],
server = Global.SingboxEchDNSTag,
domain = echDomain is not null ? new List<string> { echDomain } : null,
});
}
else if (node?.ConfigType.IsGroupType() == true)
{
var queryServerNames = (await GroupProfileManager.GetAllChildEchQuerySni(node)).ToList();
if (queryServerNames.Count > 0)
{
singboxConfig.dns.rules.Add(new()
{
query_type = [64, 65],
server = Global.SingboxEchDNSTag,
domain = queryServerNames,
});
}
}
if (simpleDNSItem.BlockBindingQuery == true)
{
singboxConfig.dns.rules.Add(new()
{ {
query_type = [64, 65], query_type = [64, 65],
action = "predefined", action = "predefined",
@ -264,7 +224,7 @@ public partial class CoreConfigSingboxService
}); });
} }
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true) if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == true)
{ {
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
fakeipFilterRule.invert = true; fakeipFilterRule.invert = true;
@ -284,13 +244,13 @@ public partial class CoreConfigSingboxService
] ]
}; };
singboxConfig.dns.rules.Add(rule4Fake); _coreConfig.dns.rules.Add(rule4Fake);
} }
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = context.RoutingItem;
if (routing == null) if (routing == null)
{ {
return 0; return;
} }
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? []; var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
@ -298,9 +258,9 @@ public partial class CoreConfigSingboxService
var expectedIPsRegions = new List<string>(); var expectedIPsRegions = new List<string>();
var regionNames = new HashSet<string>(); var regionNames = new HashSet<string>();
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) if (!string.IsNullOrEmpty(simpleDnsItem?.DirectExpectedIPs))
{ {
var ipItems = simpleDNSItem.DirectExpectedIPs var ipItems = simpleDnsItem.DirectExpectedIPs
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim()) .Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s)) .Where(s => !string.IsNullOrEmpty(s))
@ -348,7 +308,7 @@ public partial class CoreConfigSingboxService
if (item.OutboundTag == Global.DirectTag) if (item.OutboundTag == Global.DirectTag)
{ {
rule.server = Global.SingboxDirectDNSTag; rule.server = Global.SingboxDirectDNSTag;
rule.strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Freedom); rule.strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom);
if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0) if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0)
{ {
@ -373,31 +333,46 @@ public partial class CoreConfigSingboxService
} }
else else
{ {
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false) if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == false)
{ {
var rule4Fake = JsonUtils.DeepCopy(rule); var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = Global.SingboxFakeDNSTag; rule4Fake.server = Global.SingboxFakeDNSTag;
rule4Fake.query_type = new List<int> { 1, 28 }; // A and AAAA rule4Fake.query_type = new List<int> { 1, 28 }; // A and AAAA
rule4Fake.rewrite_ttl = 1; rule4Fake.rewrite_ttl = 1;
singboxConfig.dns.rules.Add(rule4Fake); _coreConfig.dns.rules.Add(rule4Fake);
} }
rule.server = Global.SingboxRemoteDNSTag; rule.server = Global.SingboxRemoteDNSTag;
rule.strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Proxy); rule.strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Proxy);
} }
singboxConfig.dns.rules.Add(rule); _coreConfig.dns.rules.Add(rule);
} }
return 0;
} }
private async Task<int> GenDnsCompatible(ProfileItem? node, SingboxConfig singboxConfig) private void GenMinimizedDns()
{
GenDnsServers();
foreach (var server in _coreConfig.dns!.servers.Where(s => !string.IsNullOrEmpty(s.detour)).ToList())
{
_coreConfig.dns.servers.Remove(server);
}
_coreConfig.dns ??= new();
_coreConfig.dns.rules ??= [];
_coreConfig.dns.rules.Clear();
_coreConfig.dns.final = Global.SingboxDirectDNSTag;
_coreConfig.route.default_domain_resolver = new()
{
server = Global.SingboxDirectDNSTag,
};
}
private void GenDnsCustom()
{ {
try try
{ {
var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); var item = context.RawDnsItem;
var strDNS = string.Empty; var strDNS = string.Empty;
if (_config.TunModeItem.EnableTun) if (context.IsTunEnabled)
{ {
strDNS = string.IsNullOrEmpty(item?.TunDNS) ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) : item?.TunDNS; strDNS = string.IsNullOrEmpty(item?.TunDNS) ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) : item?.TunDNS;
} }
@ -409,61 +384,33 @@ public partial class CoreConfigSingboxService
var dns4Sbox = JsonUtils.Deserialize<Dns4Sbox>(strDNS); var dns4Sbox = JsonUtils.Deserialize<Dns4Sbox>(strDNS);
if (dns4Sbox is null) if (dns4Sbox is null)
{ {
return 0; return;
} }
singboxConfig.dns = dns4Sbox; _coreConfig.dns = dns4Sbox;
if (dns4Sbox.servers?.Count > 0 &&
if (dns4Sbox.servers != null && dns4Sbox.servers.Count > 0 && dns4Sbox.servers.First().address.IsNullOrEmpty()) dns4Sbox.servers.First().address.IsNullOrEmpty())
{ {
await GenDnsDomainsCompatible(singboxConfig, item); GenDnsProtectCustom();
} }
else else
{ {
await GenDnsDomainsLegacyCompatible(singboxConfig, item); GenDnsProtectCustomLegacy();
} }
await GenOutboundDnsRule(node, singboxConfig);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private async Task<int> GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem) private void GenDnsProtectCustom()
{ {
var dns4Sbox = singboxConfig.dns ?? new(); var dnsItem = context.RawDnsItem;
var dns4Sbox = _coreConfig.dns ?? new();
dns4Sbox.servers ??= []; dns4Sbox.servers ??= [];
dns4Sbox.rules ??= []; dns4Sbox.rules ??= [];
var tag = Global.SingboxLocalDNSTag; var tag = Global.SingboxLocalDNSTag;
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
var localDnsServer = ParseDnsAddress(finalDnsAddress);
localDnsServer.tag = tag;
dns4Sbox.servers.Add(localDnsServer);
singboxConfig.dns = dns4Sbox;
return await Task.FromResult(0);
}
private async Task<int> GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem)
{
var dns4Sbox = singboxConfig.dns ?? new();
dns4Sbox.servers ??= [];
dns4Sbox.rules ??= [];
var tag = Global.SingboxLocalDNSTag;
dns4Sbox.servers.Add(new()
{
tag = tag,
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
detour = Global.DirectTag,
strategy = string.IsNullOrEmpty(dnsItem?.DomainStrategy4Freedom) ? null : dnsItem?.DomainStrategy4Freedom,
});
dns4Sbox.rules.Insert(0, new() dns4Sbox.rules.Insert(0, new()
{ {
server = tag, server = tag,
@ -475,56 +422,41 @@ public partial class CoreConfigSingboxService
clash_mode = ERuleMode.Global.ToString() clash_mode = ERuleMode.Global.ToString()
}); });
var lstDomain = singboxConfig.outbounds var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
.Where(t => t.server.IsNotEmpty() && Utils.IsDomain(t.server))
.Select(t => t.server)
.Distinct()
.ToList();
if (lstDomain != null && lstDomain.Count > 0)
{
dns4Sbox.rules.Insert(0, new()
{
server = tag,
domain = lstDomain
});
}
singboxConfig.dns = dns4Sbox; var localDnsServer = ParseDnsAddress(finalDnsAddress);
return await Task.FromResult(0); localDnsServer.tag = tag;
dns4Sbox.servers.Add(localDnsServer);
dns4Sbox.rules.Insert(0, BuildProtectDomainRule());
_coreConfig.dns = dns4Sbox;
} }
private async Task<int> GenOutboundDnsRule(ProfileItem? node, SingboxConfig singboxConfig) private void GenDnsProtectCustomLegacy()
{ {
if (node == null) GenDnsProtectCustom();
{
return 0;
}
List<string> domain = new(); _coreConfig.dns?.servers?.RemoveAll(s => s.tag == Global.SingboxLocalDNSTag);
if (Utils.IsDomain(node.Address)) // normal outbound var dnsItem = context.RawDnsItem;
var localDnsServer = new Server4Sbox()
{ {
domain.Add(node.Address); address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress)
} ? Global.DomainPureIPDNSAddress.FirstOrDefault()
if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty()) // Tun2SocksAddress : dnsItem?.DomainDNSAddress,
{ tag = Global.SingboxLocalDNSTag,
domain.AddRange(Utils.String2List(node.SpiderX) detour = Global.DirectTag,
.Where(Utils.IsDomain) };
.Distinct() _coreConfig.dns?.servers?.Add(localDnsServer);
.ToList()); }
}
if (domain.Count == 0)
{
return 0;
}
singboxConfig.dns.rules ??= new List<Rule4Sbox>(); private Rule4Sbox BuildProtectDomainRule()
singboxConfig.dns.rules.Insert(0, new Rule4Sbox {
return new()
{ {
server = Global.SingboxLocalDNSTag, server = Global.SingboxLocalDNSTag,
domain = domain, domain = context.ProtectDomainList.ToList(),
}); };
return await Task.FromResult(0);
} }
private static Server4Sbox? ParseDnsAddress(string address) private static Server4Sbox? ParseDnsAddress(string address)

View file

@ -2,15 +2,16 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<int> GenInbounds(SingboxConfig singboxConfig) private void GenInbounds()
{ {
try try
{ {
var listen = "0.0.0.0"; var listen = "0.0.0.0";
singboxConfig.inbounds = []; var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
_coreConfig.inbounds = [];
if (!_config.TunModeItem.EnableTun if (!context.IsTunEnabled
|| (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && AppManager.Instance.RunningCoreType == ECoreType.sing_box)) || (context.IsTunEnabled && _node.Port != listenPort))
{ {
var inbound = new Inbound4Sbox() var inbound = new Inbound4Sbox()
{ {
@ -18,23 +19,23 @@ public partial class CoreConfigSingboxService
tag = EInboundProtocol.socks.ToString(), tag = EInboundProtocol.socks.ToString(),
listen = Global.Loopback, listen = Global.Loopback,
}; };
singboxConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
inbound.listen_port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); inbound.listen_port = listenPort;
if (_config.Inbound.First().SecondLocalPortEnabled) if (_config.Inbound.First().SecondLocalPortEnabled)
{ {
var inbound2 = GetInbound(inbound, EInboundProtocol.socks2, true); var inbound2 = BuildInbound(inbound, EInboundProtocol.socks2, true);
singboxConfig.inbounds.Add(inbound2); _coreConfig.inbounds.Add(inbound2);
} }
if (_config.Inbound.First().AllowLANConn) if (_config.Inbound.First().AllowLANConn)
{ {
if (_config.Inbound.First().NewPort4LAN) if (_config.Inbound.First().NewPort4LAN)
{ {
var inbound3 = GetInbound(inbound, EInboundProtocol.socks3, true); var inbound3 = BuildInbound(inbound, EInboundProtocol.socks3, true);
inbound3.listen = listen; inbound3.listen = listen;
singboxConfig.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())
@ -49,7 +50,7 @@ public partial class CoreConfigSingboxService
} }
} }
if (_config.TunModeItem.EnableTun) if (context.IsTunEnabled)
{ {
if (_config.TunModeItem.Mtu <= 0) if (_config.TunModeItem.Mtu <= 0)
{ {
@ -71,17 +72,16 @@ public partial class CoreConfigSingboxService
tunInbound.address = ["172.18.0.1/30"]; tunInbound.address = ["172.18.0.1/30"];
} }
singboxConfig.inbounds.Add(tunInbound); _coreConfig.inbounds.Add(tunInbound);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks) private Inbound4Sbox BuildInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks)
{ {
var inbound = JsonUtils.DeepCopy(inItem); var inbound = JsonUtils.DeepCopy(inItem);
inbound.tag = protocol.ToString(); inbound.tag = protocol.ToString();

View file

@ -2,7 +2,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<int> GenLog(SingboxConfig singboxConfig) private void GenLog()
{ {
try try
{ {
@ -11,11 +11,11 @@ public partial class CoreConfigSingboxService
case "debug": case "debug":
case "info": case "info":
case "error": case "error":
singboxConfig.log.level = _config.CoreBasicItem.Loglevel; _coreConfig.log.level = _config.CoreBasicItem.Loglevel;
break; break;
case "warning": case "warning":
singboxConfig.log.level = "warn"; _coreConfig.log.level = "warn";
break; break;
default: default:
@ -23,18 +23,17 @@ public partial class CoreConfigSingboxService
} }
if (_config.CoreBasicItem.Loglevel == Global.None) if (_config.CoreBasicItem.Loglevel == Global.None)
{ {
singboxConfig.log.disabled = true; _coreConfig.log.disabled = true;
} }
if (_config.CoreBasicItem.LogEnabled) if (_config.CoreBasicItem.LogEnabled)
{ {
var dtNow = DateTime.Now; var dtNow = DateTime.Now;
singboxConfig.log.output = Utils.GetLogPath($"sbox_{dtNow:yyyy-MM-dd}.txt"); _coreConfig.log.output = Utils.GetLogPath($"sbox_{dtNow:yyyy-MM-dd}.txt");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
} }

View file

@ -2,23 +2,23 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<int> GenRouting(SingboxConfig singboxConfig) private void GenRouting()
{ {
try try
{ {
singboxConfig.route.final = Global.ProxyTag; _coreConfig.route.final = Global.ProxyTag;
var simpleDnsItem = _config.SimpleDNSItem; var simpleDnsItem = context.SimpleDnsItem;
var defaultDomainResolverTag = Global.SingboxDirectDNSTag; var defaultDomainResolverTag = Global.SingboxDirectDNSTag;
var directDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom); var directDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom);
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); var rawDNSItem = context.RawDnsItem;
if (rawDNSItem is { Enabled: true }) if (rawDNSItem is { Enabled: true })
{ {
defaultDomainResolverTag = Global.SingboxLocalDNSTag; defaultDomainResolverTag = Global.SingboxLocalDNSTag;
directDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom; directDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom;
} }
singboxConfig.route.default_domain_resolver = new() _coreConfig.route.default_domain_resolver = new()
{ {
server = defaultDomainResolverTag, server = defaultDomainResolverTag,
strategy = directDnsStrategy strategy = directDnsStrategy
@ -26,23 +26,23 @@ public partial class CoreConfigSingboxService
if (_config.TunModeItem.EnableTun) if (_config.TunModeItem.EnableTun)
{ {
singboxConfig.route.auto_detect_interface = true; _coreConfig.route.auto_detect_interface = true;
var tunRules = JsonUtils.Deserialize<List<Rule4Sbox>>(EmbedUtils.GetEmbedText(Global.TunSingboxRulesFileName)); var tunRules = JsonUtils.Deserialize<List<Rule4Sbox>>(EmbedUtils.GetEmbedText(Global.TunSingboxRulesFileName));
if (tunRules != null) if (tunRules != null)
{ {
singboxConfig.route.rules.AddRange(tunRules); _coreConfig.route.rules.AddRange(tunRules);
} }
GenRoutingDirectExe(out var lstDnsExe, out var lstDirectExe); var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe();
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
port = new() { 53 }, port = [53],
action = "hijack-dns", action = "hijack-dns",
process_name = lstDnsExe process_name = lstDnsExe
}); });
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,
process_name = lstDirectExe process_name = lstDirectExe
@ -51,66 +51,62 @@ public partial class CoreConfigSingboxService
if (_config.Inbound.First().SniffingEnabled) if (_config.Inbound.First().SniffingEnabled)
{ {
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
action = "sniff" action = "sniff"
}); });
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
protocol = new() { "dns" }, protocol = ["dns"],
action = "hijack-dns" action = "hijack-dns"
}); });
} }
else else
{ {
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
port = new() { 53 }, port = [53],
network = new() { "udp" }, network = ["udp"],
action = "hijack-dns" action = "hijack-dns"
}); });
} }
var hostsDomains = new List<string>(); var hostsDomains = new List<string>();
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); if (rawDNSItem is not { Enabled: true })
if (dnsItem == null || !dnsItem.Enabled)
{ {
var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts); var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts);
hostsDomains.AddRange(userHostsMap.Select(kvp => kvp.Key)); hostsDomains.AddRange(userHostsMap.Select(kvp => kvp.Key));
if (simpleDnsItem.UseSystemHosts == true) if (simpleDnsItem.UseSystemHosts == true)
{ {
var systemHostsMap = Utils.GetSystemHosts(); var systemHostsMap = Utils.GetSystemHosts();
foreach (var kvp in systemHostsMap) hostsDomains.AddRange(systemHostsMap.Select(kvp => kvp.Key));
{
hostsDomains.Add(kvp.Key);
}
} }
} }
if (hostsDomains.Count > 0) if (hostsDomains.Count > 0)
{ {
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
action = "resolve", action = "resolve",
domain = hostsDomains, domain = hostsDomains,
}); });
} }
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,
clash_mode = ERuleMode.Direct.ToString() clash_mode = ERuleMode.Direct.ToString()
}); });
singboxConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.ProxyTag, outbound = Global.ProxyTag,
clash_mode = ERuleMode.Global.ToString() clash_mode = ERuleMode.Global.ToString()
}); });
var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty(); var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.NullIfEmpty();
var defaultRouting = await ConfigHandler.GetDefaultRouting(_config); var routing = context.RoutingItem;
if (defaultRouting.DomainStrategy4Singbox.IsNotEmpty()) if (routing.DomainStrategy4Singbox.IsNotEmpty())
{ {
domainStrategy = defaultRouting.DomainStrategy4Singbox; domainStrategy = routing.DomainStrategy4Singbox;
} }
var resolveRule = new Rule4Sbox var resolveRule = new Rule4Sbox
{ {
@ -119,10 +115,9 @@ public partial class CoreConfigSingboxService
}; };
if (_config.RoutingBasicItem.DomainStrategy == Global.IPOnDemand) if (_config.RoutingBasicItem.DomainStrategy == Global.IPOnDemand)
{ {
singboxConfig.route.rules.Add(resolveRule); _coreConfig.route.rules.Add(resolveRule);
} }
var routing = await ConfigHandler.GetDefaultRouting(_config);
var ipRules = new List<RulesItem>(); var ipRules = new List<RulesItem>();
if (routing != null) if (routing != null)
{ {
@ -139,7 +134,7 @@ public partial class CoreConfigSingboxService
continue; continue;
} }
await GenRoutingUserRule(item1, singboxConfig); GenRoutingUserRule(item1);
if (item1.Ip?.Count > 0) if (item1.Ip?.Count > 0)
{ {
@ -149,10 +144,10 @@ public partial class CoreConfigSingboxService
} }
if (_config.RoutingBasicItem.DomainStrategy == Global.IPIfNonMatch) if (_config.RoutingBasicItem.DomainStrategy == Global.IPIfNonMatch)
{ {
singboxConfig.route.rules.Add(resolveRule); _coreConfig.route.rules.Add(resolveRule);
foreach (var item2 in ipRules) foreach (var item2 in ipRules)
{ {
await GenRoutingUserRule(item2, singboxConfig); GenRoutingUserRule(item2);
} }
} }
} }
@ -160,10 +155,9 @@ public partial class CoreConfigSingboxService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private void GenRoutingDirectExe(out List<string> lstDnsExe, out List<string> lstDirectExe) private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe()
{ {
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@ -187,20 +181,22 @@ public partial class CoreConfigSingboxService
} }
} }
lstDnsExe = new List<string>(dnsExeSet); var lstDnsExe = new List<string>(dnsExeSet);
lstDirectExe = new List<string>(directExeSet); var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
} }
private async Task<int> GenRoutingUserRule(RulesItem item, SingboxConfig singboxConfig) private void GenRoutingUserRule(RulesItem? item)
{ {
try try
{ {
if (item == null) if (item == null)
{ {
return 0; return;
} }
item.OutboundTag = await GenRoutingUserRuleOutbound(item.OutboundTag, singboxConfig); item.OutboundTag = GenRoutingUserRuleOutbound(item.OutboundTag ?? Global.ProxyTag);
var rules = singboxConfig.route.rules; var rules = _coreConfig.route.rules;
var rule = new Rule4Sbox(); var rule = new Rule4Sbox();
if (item.OutboundTag == "block") if (item.OutboundTag == "block")
@ -326,10 +322,9 @@ public partial class CoreConfigSingboxService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
private bool ParseV2Domain(string domain, Rule4Sbox rule) private static bool ParseV2Domain(string domain, Rule4Sbox rule)
{ {
if (domain.StartsWith('#') || domain.StartsWith("ext:") || domain.StartsWith("ext-domain:")) if (domain.StartsWith('#') || domain.StartsWith("ext:") || domain.StartsWith("ext-domain:"))
{ {
@ -368,7 +363,7 @@ public partial class CoreConfigSingboxService
return true; return true;
} }
private bool ParseV2Address(string address, Rule4Sbox rule) private static bool ParseV2Address(string address, Rule4Sbox rule)
{ {
if (address.StartsWith("ext:") || address.StartsWith("ext-ip:")) if (address.StartsWith("ext:") || address.StartsWith("ext-ip:"))
{ {
@ -401,14 +396,14 @@ public partial class CoreConfigSingboxService
return true; return true;
} }
private async Task<string?> GenRoutingUserRuleOutbound(string outboundTag, SingboxConfig singboxConfig) private string GenRoutingUserRuleOutbound(string outboundTag)
{ {
if (Global.OutboundTags.Contains(outboundTag)) if (Global.OutboundTags.Contains(outboundTag))
{ {
return outboundTag; return outboundTag;
} }
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); var node = context.AllProxiesMap.GetValueOrDefault($"remark:{outboundTag}");
if (node == null if (node == null
|| (!Global.SingboxSupportConfigType.Contains(node.ConfigType) || (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
@ -418,39 +413,15 @@ public partial class CoreConfigSingboxService
} }
var tag = $"{node.IndexId}-{Global.ProxyTag}"; var tag = $"{node.IndexId}-{Global.ProxyTag}";
if (singboxConfig.outbounds.Any(o => o.tag == tag) if (_coreConfig.outbounds.Any(o => o.tag.StartsWith(tag))
|| (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag))) || (_coreConfig.endpoints != null && _coreConfig.endpoints.Any(e => e.tag.StartsWith(tag))))
{ {
return tag; return tag;
} }
if (node.ConfigType.IsGroupType()) var proxyOutbounds = new CoreConfigSingboxService(context with { Node = node, }).BuildAllProxyOutbounds(tag);
{ FillRangeProxy(proxyOutbounds, _coreConfig, false);
var ret = await GenGroupOutbound(node, singboxConfig, tag);
if (ret == 0)
{
return tag;
}
return Global.ProxyTag;
}
var server = await GenServer(node); return tag;
if (server is null)
{
return Global.ProxyTag;
}
server.tag = tag;
if (server is Endpoints4Sbox endpoint)
{
singboxConfig.endpoints ??= new();
singboxConfig.endpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
singboxConfig.outbounds.Add(outbound);
}
return server.tag;
} }
} }

View file

@ -2,7 +2,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<int> ConvertGeo2Ruleset(SingboxConfig singboxConfig) private void ConvertGeo2Ruleset()
{ {
static void AddRuleSets(List<string> ruleSets, List<string>? rule_set) static void AddRuleSets(List<string> ruleSets, List<string>? rule_set)
{ {
@ -16,14 +16,14 @@ public partial class CoreConfigSingboxService
var ruleSets = new List<string>(); var ruleSets = new List<string>();
//convert route geosite & geoip to ruleset //convert route geosite & geoip to ruleset
foreach (var rule in singboxConfig.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 ??= new List<string>();
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 singboxConfig.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 ??= new List<string>();
rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList()); rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList());
@ -32,24 +32,24 @@ public partial class CoreConfigSingboxService
} }
//convert dns geosite & geoip to ruleset //convert dns geosite & geoip to ruleset
foreach (var rule in singboxConfig.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 ??= new List<string>();
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 singboxConfig.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 ??= new List<string>();
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 singboxConfig.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() ?? [])
{ {
AddRuleSets(ruleSets, dnsRule.rule_set); AddRuleSets(ruleSets, dnsRule.rule_set);
} }
//rules in rules //rules in rules
foreach (var item in singboxConfig.dns?.rules.Where(t => t.rules?.Count > 0).Select(t => t.rules).ToList() ?? []) foreach (var item in _coreConfig.dns?.rules.Where(t => t.rules?.Count > 0).Select(t => t.rules).ToList() ?? [])
{ {
foreach (var item2 in item ?? []) foreach (var item2 in item ?? [])
{ {
@ -60,7 +60,7 @@ public partial class CoreConfigSingboxService
//load custom ruleset file //load custom ruleset file
List<Ruleset4Sbox> customRulesets = []; List<Ruleset4Sbox> customRulesets = [];
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = context.RoutingItem;
if (routing.CustomRulesetPath4Singbox.IsNotEmpty()) if (routing.CustomRulesetPath4Singbox.IsNotEmpty())
{ {
var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox); var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox);
@ -78,7 +78,7 @@ public partial class CoreConfigSingboxService
var localSrss = Utils.GetBinPath("srss"); var localSrss = Utils.GetBinPath("srss");
//Add ruleset srs //Add ruleset srs
singboxConfig.route.rule_set = []; _coreConfig.route.rule_set = [];
foreach (var item in new HashSet<string>(ruleSets)) foreach (var item in new HashSet<string>(ruleSets))
{ {
if (item.IsNullOrEmpty()) if (item.IsNullOrEmpty())
@ -113,9 +113,7 @@ public partial class CoreConfigSingboxService
}; };
} }
} }
singboxConfig.route.rule_set.Add(customRuleset); _coreConfig.route.rule_set.Add(customRuleset);
} }
return 0;
} }
} }

View file

@ -2,12 +2,12 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private async Task<int> GenExperimental(SingboxConfig singboxConfig) private void GenExperimental()
{ {
//if (_config.guiItem.enableStatistics) //if (_config.guiItem.enableStatistics)
{ {
singboxConfig.experimental ??= new Experimental4Sbox(); _coreConfig.experimental ??= new Experimental4Sbox();
singboxConfig.experimental.clash_api = new Clash_Api4Sbox() _coreConfig.experimental.clash_api = new Clash_Api4Sbox()
{ {
external_controller = $"{Global.Loopback}:{AppManager.Instance.StatePort2}", external_controller = $"{Global.Loopback}:{AppManager.Instance.StatePort2}",
}; };
@ -15,15 +15,13 @@ public partial class CoreConfigSingboxService
if (_config.CoreBasicItem.EnableCacheFile4Sbox) if (_config.CoreBasicItem.EnableCacheFile4Sbox)
{ {
singboxConfig.experimental ??= new Experimental4Sbox(); _coreConfig.experimental ??= new Experimental4Sbox();
singboxConfig.experimental.cache_file = new CacheFile4Sbox() _coreConfig.experimental.cache_file = new CacheFile4Sbox()
{ {
enabled = true, enabled = true,
path = Utils.GetBinPath("cache.db"), path = Utils.GetBinPath("cache.db"),
store_fakeip = _config.SimpleDNSItem.FakeIP == true store_fakeip = context.SimpleDnsItem.FakeIP == true
}; };
} }
return await Task.FromResult(0);
} }
} }

View file

@ -1,44 +1,39 @@
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService(Config config) public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
private readonly Config _config = config;
private static readonly string _tag = "CoreConfigV2rayService"; private static readonly string _tag = "CoreConfigV2rayService";
private readonly Config _config = context.AppConfig;
private readonly ProfileItem _node = context.Node;
private V2rayConfig _coreConfig = new();
#region public gen function #region public gen function
public async Task<RetResult> GenerateClientConfigContent(ProfileItem node) public RetResult GenerateClientConfigContent()
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (node == null if (context.IsTunEnabled && context.TunProtectSsPort > 0 && context.ProxyRelaySsPort > 0)
|| !node.IsValid()) {
return GenerateClientProxyRelayConfig();
}
if (_node == null
|| !_node.IsValid())
{ {
ret.Msg = ResUI.CheckServerSettings; ret.Msg = ResUI.CheckServerSettings;
return ret; return ret;
} }
if (node.GetNetwork() is nameof(ETransport.quic)) if (_node.GetNetwork() is nameof(ETransport.quic))
{ {
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}";
return ret; return ret;
} }
ret.Msg = ResUI.InitialConfiguration; ret.Msg = ResUI.InitialConfiguration;
if (node.ConfigType.IsGroupType())
{
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
return await GenerateClientMultipleLoadConfig(node);
case EConfigType.ProxyChain:
return await GenerateClientChainConfig(node);
}
}
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
@ -46,30 +41,34 @@ public partial class CoreConfigV2rayService(Config config)
return ret; return ret;
} }
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result); _coreConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null) if (_coreConfig == null)
{ {
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenLog(v2rayConfig); GenLog();
await GenInbounds(v2rayConfig); GenInbounds();
await GenOutbound(node, v2rayConfig.outbounds.First()); GenOutbounds();
await GenMoreOutbounds(node, v2rayConfig); GenRouting();
await GenRouting(v2rayConfig); GenDns();
await GenDns(node, v2rayConfig); GenStatistic();
await GenStatistic(v2rayConfig); var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag))
{
_coreConfig.routing.rules.Add(finalRule);
}
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig); ret.Data = ApplyFullConfigTemplate();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -80,18 +79,11 @@ public partial class CoreConfigV2rayService(Config config)
} }
} }
public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode) public RetResult GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration; ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
@ -102,193 +94,19 @@ public partial class CoreConfigV2rayService(Config config)
return ret; return ret;
} }
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result); _coreConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null) if (_coreConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
{ {
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenRouting(v2rayConfig); var (lstIpEndPoints, lstTcpConns) = Utils.GetActiveNetworkInfo();
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}"; GenLog();
_coreConfig.inbounds.Clear();
//add rule _coreConfig.outbounds.Clear();
var rules = v2rayConfig.routing.rules; _coreConfig.routing.rules.Clear();
if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0))
{
var balancerTagSet = v2rayConfig.routing.balancers
.Select(b => b.tag)
.ToHashSet();
foreach (var rule in rules)
{
if (rule.outboundTag == null)
{
continue;
}
if (balancerTagSet.Contains(rule.outboundTag))
{
rule.balancerTag = rule.outboundTag;
rule.outboundTag = null;
continue;
}
var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix;
if (balancerTagSet.Contains(outboundWithSuffix))
{
rule.balancerTag = outboundWithSuffix;
rule.outboundTag = null;
}
}
}
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
{
v2rayConfig.routing.rules.Add(new()
{
ip = ["0.0.0.0/0", "::/0"],
balancerTag = defaultBalancerTag,
type = "field"
});
}
else
{
v2rayConfig.routing.rules.Add(new()
{
network = "tcp,udp",
balancerTag = defaultBalancerTag,
type = "field"
});
}
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientSpeedtestConfig(List<ServerTestItem> selecteds)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
List<IPEndPoint> lstIpEndPoints = new();
List<TcpConnectionInformation> lstTcpConns = new();
try
{
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners());
lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners());
lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await GenLog(v2rayConfig);
v2rayConfig.inbounds.Clear();
v2rayConfig.outbounds.Clear();
v2rayConfig.routing.rules.Clear();
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest); var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
@ -302,8 +120,9 @@ public partial class CoreConfigV2rayService(Config config)
{ {
continue; continue;
} }
var item = await AppManager.Instance.GetProfileItem(it.IndexId); var actIndexId = context.ServerTestItemMap.GetValueOrDefault(it.IndexId, it.IndexId);
if (item is null || item.IsComplex() || !item.IsValid()) var item = context.AllProxiesMap.GetValueOrDefault(actIndexId);
if (item is null || item.ConfigType is EConfigType.Custom || !item.IsValid())
{ {
continue; continue;
} }
@ -342,27 +161,40 @@ public partial class CoreConfigV2rayService(Config config)
protocol = EInboundProtocol.mixed.ToString(), protocol = EInboundProtocol.mixed.ToString(),
}; };
inbound.tag = inbound.protocol + inbound.port.ToString(); inbound.tag = inbound.protocol + inbound.port.ToString();
v2rayConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
var tag = Global.ProxyTag + inbound.port.ToString();
var isBalancer = false;
//outbound //outbound
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var proxyOutbounds =
await GenOutbound(item, outbound); new CoreConfigV2rayService(context with { Node = item }).BuildAllProxyOutbounds(tag);
outbound.tag = Global.ProxyTag + inbound.port.ToString(); _coreConfig.outbounds.AddRange(proxyOutbounds);
v2rayConfig.outbounds.Add(outbound); if (proxyOutbounds.Count(n => n.tag.StartsWith(tag)) > 1)
{
isBalancer = true;
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
GenObservatory(multipleLoad, tag);
GenBalancer(multipleLoad, tag);
}
//rule //rule
RulesItem4Ray rule = new() RulesItem4Ray rule = new()
{ {
inboundTag = new List<string> { inbound.tag }, inboundTag = new List<string> { inbound.tag },
outboundTag = outbound.tag, outboundTag = tag,
type = "field" type = "field"
}; };
v2rayConfig.routing.rules.Add(rule); if (isBalancer)
{
rule.balancerTag = tag;
rule.outboundTag = null;
}
_coreConfig.routing.rules.Add(rule);
} }
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(v2rayConfig); ret.Data = JsonUtils.Serialize(_coreConfig);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@ -373,21 +205,21 @@ public partial class CoreConfigV2rayService(Config config)
} }
} }
public async Task<RetResult> GenerateClientSpeedtestConfig(ProfileItem node, int port) public RetResult GenerateClientSpeedtestConfig(int port)
{ {
var ret = new RetResult(); var ret = new RetResult();
try try
{ {
if (node == null if (_node == null
|| !node.IsValid()) || !_node.IsValid())
{ {
ret.Msg = ResUI.CheckServerSettings; ret.Msg = ResUI.CheckServerSettings;
return ret; return ret;
} }
if (node.GetNetwork() is nameof(ETransport.quic)) if (_node.GetNetwork() is nameof(ETransport.quic))
{ {
ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}";
return ret; return ret;
} }
@ -398,20 +230,20 @@ public partial class CoreConfigV2rayService(Config config)
return ret; return ret;
} }
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result); _coreConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null) if (_coreConfig == null)
{ {
ret.Msg = ResUI.FailedGenDefaultConfiguration; ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret; return ret;
} }
await GenLog(v2rayConfig); GenLog();
await GenOutbound(node, v2rayConfig.outbounds.First()); GenOutbounds();
await GenMoreOutbounds(node, v2rayConfig);
v2rayConfig.routing.rules.Clear(); _coreConfig.routing.domainStrategy = Global.AsIs;
v2rayConfig.inbounds.Clear(); _coreConfig.routing.rules.Clear();
v2rayConfig.inbounds.Add(new() _coreConfig.inbounds.Clear();
_coreConfig.inbounds.Add(new()
{ {
tag = $"{EInboundProtocol.socks}{port}", tag = $"{EInboundProtocol.socks}{port}",
listen = Global.Loopback, listen = Global.Loopback,
@ -419,9 +251,112 @@ public partial class CoreConfigV2rayService(Config config)
protocol = EInboundProtocol.mixed.ToString(), protocol = EInboundProtocol.mixed.ToString(),
}); });
_coreConfig.routing.rules.Add(BuildFinalRule());
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(v2rayConfig); ret.Data = JsonUtils.Serialize(_coreConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public RetResult GenerateClientProxyRelayConfig()
{
var ret = new RetResult();
try
{
if (_node == null
|| !_node.IsValid())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
if (_node.GetNetwork() is nameof(ETransport.quic))
{
ret.Msg = ResUI.Incorrectconfiguration + $" - {_node.GetNetwork()}";
return ret;
}
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
if (result.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
_coreConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (_coreConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
GenLog();
_coreConfig.outbounds.Clear();
GenOutbounds();
var protectNode = new ProfileItem()
{
CoreType = ECoreType.Xray,
ConfigType = EConfigType.Shadowsocks,
Address = Global.Loopback,
Port = context.TunProtectSsPort,
Password = Global.None,
};
protectNode.SetProtocolExtra(protectNode.GetProtocolExtra() with
{
SsMethod = Global.None,
});
foreach (var outbound in _coreConfig.outbounds.Where(outbound => outbound.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
{
outbound.streamSettings ??= new StreamSettings4Ray();
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
outbound.streamSettings.sockopt.dialerProxy = "tun-project-ss";
}
_coreConfig.outbounds.Add(new CoreConfigV2rayService(context with
{
Node = protectNode,
}).BuildProxyOutbound("tun-project-ss"));
var hasBalancer = _coreConfig.routing.balancers is { Count: > 0 };
_coreConfig.routing.rules =
[
new()
{
inboundTag = new List<string> { "proxy-relay-ss" },
outboundTag = hasBalancer ? null : Global.ProxyTag,
balancerTag = hasBalancer ? Global.ProxyTag + Global.BalancerTagSuffix: null,
type = "field"
}
];
_coreConfig.inbounds.Clear();
var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!;
configNode["inbounds"]!.AsArray().Add(new
{
listen = Global.Loopback,
port = context.ProxyRelaySsPort,
protocol = "shadowsocks",
settings = new
{
network = "tcp,udp",
method = Global.None,
password = Global.None,
},
tag = "proxy-relay-ss",
});
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = JsonUtils.Serialize(configNode);
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)

View file

@ -2,17 +2,17 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) private void GenObservatory(EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{ {
// Collect all existing subject selectors from both observatories // Collect all existing subject selectors from both observatories
var subjectSelectors = new List<string>(); var subjectSelectors = new List<string>();
subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []); subjectSelectors.AddRange(_coreConfig.burstObservatory?.subjectSelector ?? []);
subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []); subjectSelectors.AddRange(_coreConfig.observatory?.subjectSelector ?? []);
// Case 1: exact match already exists -> nothing to do // Case 1: exact match already exists -> nothing to do
if (subjectSelectors.Any(baseTagName.StartsWith)) if (subjectSelectors.Any(baseTagName.StartsWith))
{ {
return await Task.FromResult(0); return;
} }
// Case 2: prefix match exists -> reuse it and move to the first position // Case 2: prefix match exists -> reuse it and move to the first position
@ -21,28 +21,28 @@ public partial class CoreConfigV2rayService
{ {
baseTagName = matched; baseTagName = matched;
if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true) if (_coreConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true)
{ {
v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName); _coreConfig.burstObservatory.subjectSelector.Remove(baseTagName);
v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName); _coreConfig.burstObservatory.subjectSelector.Insert(0, baseTagName);
} }
if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true) if (_coreConfig.observatory?.subjectSelector?.Contains(baseTagName) == true)
{ {
v2rayConfig.observatory.subjectSelector.Remove(baseTagName); _coreConfig.observatory.subjectSelector.Remove(baseTagName);
v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName); _coreConfig.observatory.subjectSelector.Insert(0, baseTagName);
} }
return await Task.FromResult(0); return;
} }
// Case 3: need to create or insert based on multipleLoad type // Case 3: need to create or insert based on multipleLoad type
if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback) if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
{ {
if (v2rayConfig.burstObservatory is null) if (_coreConfig.burstObservatory is null)
{ {
// Create new burst observatory with default ping config // Create new burst observatory with default ping config
v2rayConfig.burstObservatory = new BurstObservatory4Ray _coreConfig.burstObservatory = new BurstObservatory4Ray
{ {
subjectSelector = [baseTagName], subjectSelector = [baseTagName],
pingConfig = new() pingConfig = new()
@ -56,16 +56,16 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
v2rayConfig.burstObservatory.subjectSelector ??= new(); _coreConfig.burstObservatory.subjectSelector ??= new();
v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName); _coreConfig.burstObservatory.subjectSelector.Add(baseTagName);
} }
} }
else if (multipleLoad is EMultipleLoad.LeastPing) else if (multipleLoad is EMultipleLoad.LeastPing)
{ {
if (v2rayConfig.observatory is null) if (_coreConfig.observatory is null)
{ {
// Create new observatory with default probe config // Create new observatory with default probe config
v2rayConfig.observatory = new Observatory4Ray _coreConfig.observatory = new Observatory4Ray
{ {
subjectSelector = [baseTagName], subjectSelector = [baseTagName],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
@ -75,15 +75,13 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
v2rayConfig.observatory.subjectSelector ??= new(); _coreConfig.observatory.subjectSelector ??= new();
v2rayConfig.observatory.subjectSelector.Add(baseTagName); _coreConfig.observatory.subjectSelector.Add(baseTagName);
} }
} }
return await Task.FromResult(0);
} }
private async Task<string> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag) private void GenBalancer(EMultipleLoad multipleLoad, string selector = Global.ProxyTag)
{ {
var strategyType = multipleLoad switch var strategyType = multipleLoad switch
{ {
@ -107,8 +105,7 @@ public partial class CoreConfigV2rayService
}, },
tag = balancerTag, tag = balancerTag,
}; };
v2rayConfig.routing.balancers ??= new(); _coreConfig.routing.balancers ??= new();
v2rayConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);
return await Task.FromResult(balancerTag);
} }
} }

View file

@ -2,35 +2,39 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig) private string ApplyFullConfigTemplate()
{ {
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); var fullConfigTemplate = context.FullConfigTemplate;
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty()) if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
{ {
return JsonUtils.Serialize(v2rayConfig); return JsonUtils.Serialize(_coreConfig);
} }
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config); var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config);
if (fullConfigTemplateNode == null) if (fullConfigTemplateNode == null)
{ {
return JsonUtils.Serialize(v2rayConfig); return JsonUtils.Serialize(_coreConfig);
} }
// Handle balancer and rules modifications (for multiple load scenarios) // Handle balancer and rules modifications (for multiple load scenarios)
if (v2rayConfig.routing?.balancers?.Count > 0) if (_coreConfig.routing?.balancers?.Count > 0)
{ {
var balancer = v2rayConfig.routing.balancers.First(); var balancer =
_coreConfig.routing.balancers.FirstOrDefault(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix, null);
// Modify existing rules in custom config // Modify existing rules in custom config
var rulesNode = fullConfigTemplateNode["routing"]?["rules"]; if (balancer != null)
if (rulesNode != null)
{ {
foreach (var rule in rulesNode.AsArray()) var rulesNode = fullConfigTemplateNode["routing"]?["rules"];
if (rulesNode != null)
{ {
if (rule["outboundTag"]?.GetValue<string>() == Global.ProxyTag) foreach (var rule in rulesNode.AsArray())
{ {
rule.AsObject().Remove("outboundTag"); if (rule["outboundTag"]?.GetValue<string>() == Global.ProxyTag)
rule["balancerTag"] = balancer.tag; {
rule.AsObject().Remove("outboundTag");
rule["balancerTag"] = balancer.tag;
}
} }
} }
} }
@ -44,7 +48,7 @@ public partial class CoreConfigV2rayService
// 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)
{ {
if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)) is JsonArray newBalancers) if (JsonNode.Parse(JsonUtils.Serialize(_coreConfig.routing.balancers)) is JsonArray newBalancers)
{ {
foreach (var balancerNode in newBalancers) foreach (var balancerNode in newBalancers)
{ {
@ -54,33 +58,33 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
fullConfigTemplateNode["routing"]["balancers"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)); fullConfigTemplateNode["routing"]["balancers"] = JsonNode.Parse(JsonUtils.Serialize(_coreConfig.routing.balancers));
} }
} }
if (v2rayConfig.observatory != null) if (_coreConfig.observatory != null)
{ {
if (fullConfigTemplateNode["observatory"] == null) if (fullConfigTemplateNode["observatory"] == null)
{ {
fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.observatory)); fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(_coreConfig.observatory));
} }
else else
{ {
var subjectSelector = v2rayConfig.observatory.subjectSelector; var subjectSelector = _coreConfig.observatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []); subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
} }
} }
if (v2rayConfig.burstObservatory != null) if (_coreConfig.burstObservatory != null)
{ {
if (fullConfigTemplateNode["burstObservatory"] == null) if (fullConfigTemplateNode["burstObservatory"] == null)
{ {
fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.burstObservatory)); fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(_coreConfig.burstObservatory));
} }
else else
{ {
var subjectSelector = v2rayConfig.burstObservatory.subjectSelector; var subjectSelector = _coreConfig.burstObservatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []); subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
} }
@ -88,7 +92,7 @@ public partial class CoreConfigV2rayService
var customOutboundsNode = new JsonArray(); var customOutboundsNode = new JsonArray();
foreach (var outbound in v2rayConfig.outbounds) foreach (var outbound in _coreConfig.outbounds)
{ {
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom") if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom")
{ {
@ -97,8 +101,8 @@ public partial class CoreConfigV2rayService
continue; continue;
} }
} }
else if ((!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) else if (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()
&& ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() ?? true) == true)) && (outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() ?? true))
{ {
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address
?? outbound.settings?.vnext?.FirstOrDefault()?.address ?? outbound.settings?.vnext?.FirstOrDefault()?.address
@ -123,6 +127,6 @@ public partial class CoreConfigV2rayService
fullConfigTemplateNode["outbounds"] = customOutboundsNode; fullConfigTemplateNode["outbounds"] = customOutboundsNode;
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode)); return JsonUtils.Serialize(fullConfigTemplateNode);
} }
} }

View file

@ -2,46 +2,45 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenDns(ProfileItem? node, V2rayConfig v2rayConfig) private void GenDns()
{ {
try try
{ {
var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray); var item = context.RawDnsItem;
if (item is { Enabled: true }) if (item is { Enabled: true })
{ {
var result = await GenDnsCompatible(node, v2rayConfig); GenDnsCustom();
if (v2rayConfig.routing.domainStrategy != Global.IPIfNonMatch) if (_coreConfig.routing.domainStrategy != Global.IPIfNonMatch)
{ {
return result; return;
} }
// DNS routing // DNS routing
var dnsObj = JsonUtils.SerializeToNode(v2rayConfig.dns); var dnsObj = JsonUtils.SerializeToNode(_coreConfig.dns);
if (dnsObj == null) if (dnsObj == null)
{ {
return result; return;
} }
dnsObj["tag"] = Global.DnsTag; dnsObj["tag"] = Global.DnsTag;
v2rayConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(dnsObj)); _coreConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(dnsObj));
v2rayConfig.routing.rules.Add(new RulesItem4Ray _coreConfig.routing.rules.Add(new RulesItem4Ray
{ {
type = "field", type = "field",
inboundTag = new List<string> { Global.DnsTag }, inboundTag = new List<string> { Global.DnsTag },
outboundTag = Global.ProxyTag, outboundTag = Global.ProxyTag,
}); });
return;
return result;
} }
var simpleDnsItem = _config.SimpleDNSItem; var simpleDnsItem = context.SimpleDnsItem;
var dnsItem = v2rayConfig.dns is Dns4Ray dns4Ray ? dns4Ray : new Dns4Ray(); var dnsItem = _coreConfig.dns is Dns4Ray dns4Ray ? dns4Ray : new Dns4Ray();
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs; var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
//Outbound Freedom domainStrategy //Outbound Freedom domainStrategy
if (strategy4Freedom.IsNotEmpty() && strategy4Freedom != Global.AsIs) if (strategy4Freedom.IsNotEmpty() && strategy4Freedom != Global.AsIs)
{ {
var outbound = v2rayConfig.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()
@ -59,80 +58,40 @@ public partial class CoreConfigV2rayService
var xraySupportConfigTypeNames = Global.XraySupportConfigType var xraySupportConfigTypeNames = Global.XraySupportConfigType
.Select(x => x == EConfigType.Hysteria2 ? "hysteria" : Global.ProtocolTypes[x]) .Select(x => x == EConfigType.Hysteria2 ? "hysteria" : Global.ProtocolTypes[x])
.ToHashSet(); .ToHashSet();
v2rayConfig.outbounds _coreConfig.outbounds
.Where(t => xraySupportConfigTypeNames.Contains(t.protocol)) .Where(t => xraySupportConfigTypeNames.Contains(t.protocol))
.ToList() .ToList()
.ForEach(outbound => outbound.targetStrategy = strategy4Proxy); .ForEach(outbound => outbound.targetStrategy = strategy4Proxy);
} }
await GenDnsServers(node, dnsItem, simpleDnsItem); FillDnsServers(dnsItem);
await GenDnsHosts(dnsItem, simpleDnsItem); FillDnsHosts(dnsItem);
dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null; dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null;
dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null; dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null;
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) // DNS routing
var finalRule = BuildFinalRule();
dnsItem.tag = Global.DnsTag;
_coreConfig.routing.rules.Add(new()
{ {
// DNS routing type = "field",
dnsItem.tag = Global.DnsTag; inboundTag = [Global.DnsTag],
v2rayConfig.routing.rules.Add(new RulesItem4Ray outboundTag = finalRule.outboundTag,
{ balancerTag = finalRule.balancerTag
type = "field", });
inboundTag = new List<string> { Global.DnsTag },
outboundTag = Global.ProxyTag,
});
}
v2rayConfig.dns = dnsItem; _coreConfig.dns = dnsItem;
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private async Task<int> GenDnsServers(ProfileItem? node, Dns4Ray dnsItem, SimpleDNSItem simpleDNSItem) private void FillDnsServers(Dns4Ray dnsItem)
{ {
static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress) var simpleDNSItem = context.SimpleDnsItem;
{
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList() ?? new List<string> { defaultAddress };
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
}
static object? CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress);
var domainFinal = dnsAddress;
int? portFinal = null;
if (scheme.IsNullOrEmpty() || scheme.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = domain;
portFinal = port > 0 ? port : null;
}
else if (scheme.StartsWith("tcp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = scheme + "://" + domain;
portFinal = port > 0 ? port : null;
}
var dnsServer = new DnsServer4Ray
{
address = domainFinal,
port = portFinal,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
};
return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.First()); var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.First());
var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.First()); var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.First());
@ -197,9 +156,9 @@ public partial class CoreConfigV2rayService
} }
} }
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = context.RoutingItem;
List<RulesItem>? rules = null; List<RulesItem>? rules = null;
rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? []; rules = JsonUtils.Deserialize<List<RulesItem>>(routing?.RuleSet) ?? [];
foreach (var item in rules) foreach (var item in rules)
{ {
if (!item.Enabled || item.Domain is null || item.Domain.Count == 0) if (!item.Enabled || item.Domain is null || item.Domain.Count == 0)
@ -246,42 +205,13 @@ public partial class CoreConfigV2rayService
} }
} }
if (Utils.IsDomain(node?.Address)) if (context.ProtectDomainList.Count > 0)
{ {
directDomainList.Add(node.Address); directDomainList.AddRange(context.ProtectDomainList);
}
if (node?.Subid is not null)
{
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is not null)
{
foreach (var profile in new[] { subItem.PrevProfile, subItem.NextProfile })
{
var profileNode = await AppManager.Instance.GetProfileItemViaRemarks(profile);
if (profileNode is not null
&& Global.XraySupportConfigType.Contains(profileNode.ConfigType)
&& Utils.IsDomain(profileNode.Address))
{
directDomainList.Add(profileNode.Address);
}
}
}
} }
dnsItem.servers ??= []; dnsItem.servers ??= [];
void AddDnsServers(List<string> dnsAddresses, List<string> domains, List<string>? expectedIPs = null)
{
if (domains.Count > 0)
{
foreach (var dnsAddress in dnsAddresses)
{
dnsItem.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs));
}
}
}
AddDnsServers(remoteDNSAddress, proxyDomainList); AddDnsServers(remoteDNSAddress, proxyDomainList);
AddDnsServers(directDNSAddress, directDomainList); AddDnsServers(directDNSAddress, directDomainList);
AddDnsServers(remoteDNSAddress, proxyGeositeList); AddDnsServers(remoteDNSAddress, proxyGeositeList);
@ -292,23 +222,81 @@ public partial class CoreConfigV2rayService
AddDnsServers(bootstrapDNSAddress, dnsServerDomains); AddDnsServers(bootstrapDNSAddress, dnsServerDomains);
} }
var useDirectDns = rules?.LastOrDefault() is { } lastRule var useDirectDns = false;
&& lastRule.OutboundTag == Global.DirectTag
&& (lastRule.Port == "0-65535" if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
|| lastRule.Network == "tcp,udp" {
|| lastRule.Ip?.Contains("0.0.0.0/0") == true); var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
var isAnyIp = lastRule.Ip == null || lastRule.Ip.Count == 0 || lastRule.Ip.Contains("0.0.0.0/0");
var isAnyPort = string.IsNullOrEmpty(lastRule.Port) || lastRule.Port == "0-65535";
var isAnyNetwork = string.IsNullOrEmpty(lastRule.Network) || lastRule.Network == "tcp,udp";
useDirectDns = noDomain && noProcess && isAnyIp && isAnyPort && isAnyNetwork;
}
var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress; var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress;
dnsItem.servers.AddRange(defaultDnsServers); dnsItem.servers.AddRange(defaultDnsServers);
return;
return 0; static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress)
{
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList() ?? [defaultAddress];
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
}
static object? CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress);
var domainFinal = dnsAddress;
int? portFinal = null;
if (scheme.IsNullOrEmpty() || scheme.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = domain;
portFinal = port > 0 ? port : null;
}
else if (scheme.StartsWith("tcp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = scheme + "://" + domain;
portFinal = port > 0 ? port : null;
}
var dnsServer = new DnsServer4Ray
{
address = domainFinal,
port = portFinal,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
};
return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
void AddDnsServers(List<string> dnsAddresses, List<string> domains, List<string>? expectedIPs = null)
{
if (domains.Count <= 0)
{
return;
}
foreach (var dnsAddress in dnsAddresses)
{
dnsItem.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs));
}
}
} }
private async Task<int> GenDnsHosts(Dns4Ray dnsItem, SimpleDNSItem simpleDNSItem) private void FillDnsHosts(Dns4Ray dnsItem)
{ {
var simpleDNSItem = context.SimpleDnsItem;
if (simpleDNSItem.AddCommonHosts == false && simpleDNSItem.UseSystemHosts == false && simpleDNSItem.Hosts.IsNullOrEmpty()) if (simpleDNSItem.AddCommonHosts == false && simpleDNSItem.UseSystemHosts == false && simpleDNSItem.Hosts.IsNullOrEmpty())
{ {
return await Task.FromResult(0); return;
} }
dnsItem.hosts ??= new Dictionary<string, object>(); dnsItem.hosts ??= new Dictionary<string, object>();
if (simpleDNSItem.AddCommonHosts == true) if (simpleDNSItem.AddCommonHosts == true)
@ -337,14 +325,13 @@ public partial class CoreConfigV2rayService
{ {
dnsItem.hosts[kvp.Key] = kvp.Value; dnsItem.hosts[kvp.Key] = kvp.Value;
} }
return await Task.FromResult(0);
} }
private async Task<int> GenDnsCompatible(ProfileItem? node, V2rayConfig v2rayConfig) private void GenDnsCustom()
{ {
try try
{ {
var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray); var item = context.RawDnsItem;
var normalDNS = item?.NormalDNS; var normalDNS = item?.NormalDNS;
var domainStrategy4Freedom = item?.DomainStrategy4Freedom; var domainStrategy4Freedom = item?.DomainStrategy4Freedom;
if (normalDNS.IsNullOrEmpty()) if (normalDNS.IsNullOrEmpty())
@ -355,7 +342,7 @@ public partial class CoreConfigV2rayService
//Outbound Freedom domainStrategy //Outbound Freedom domainStrategy
if (domainStrategy4Freedom.IsNotEmpty()) if (domainStrategy4Freedom.IsNotEmpty())
{ {
var outbound = v2rayConfig.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();
@ -410,63 +397,37 @@ public partial class CoreConfigV2rayService
} }
} }
await GenDnsDomainsCompatible(node, obj, item); FillDnsDomainsCustom(obj);
v2rayConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(obj)); _coreConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(obj));
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private async Task<int> GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dnsItem) private void FillDnsDomainsCustom(JsonNode dns)
{ {
if (node == null)
{
return 0;
}
var servers = dns["servers"]; var servers = dns["servers"];
if (servers != null) if (servers == null)
{ {
var domainList = new List<string>(); return;
if (Utils.IsDomain(node.Address))
{
domainList.Add(node.Address);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is not null)
{
// Previous proxy
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& Global.SingboxSupportConfigType.Contains(prevNode.ConfigType)
&& Utils.IsDomain(prevNode.Address))
{
domainList.Add(prevNode.Address);
}
// Next proxy
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& Global.SingboxSupportConfigType.Contains(nextNode.ConfigType)
&& Utils.IsDomain(nextNode.Address))
{
domainList.Add(nextNode.Address);
}
}
if (domainList.Count > 0)
{
var dnsServer = new DnsServer4Ray()
{
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
skipFallback = true,
domains = domainList
};
servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer));
}
} }
return await Task.FromResult(0);
var domainList = context.ProtectDomainList;
if (domainList.Count <= 0)
{
return;
}
var dnsItem = context.RawDnsItem;
var dnsServer = new DnsServer4Ray()
{
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
skipFallback = true,
domains = domainList.ToList(),
};
servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer));
} }
} }

View file

@ -2,35 +2,35 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenInbounds(V2rayConfig v2rayConfig) private void GenInbounds()
{ {
try try
{ {
var listen = "0.0.0.0"; var listen = "0.0.0.0";
v2rayConfig.inbounds = []; _coreConfig.inbounds = [];
var inbound = GetInbound(_config.Inbound.First(), EInboundProtocol.socks, true); var inbound = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks, true);
v2rayConfig.inbounds.Add(inbound); _coreConfig.inbounds.Add(inbound);
if (_config.Inbound.First().SecondLocalPortEnabled) if (_config.Inbound.First().SecondLocalPortEnabled)
{ {
var inbound2 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks2, true); var inbound2 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks2, true);
v2rayConfig.inbounds.Add(inbound2); _coreConfig.inbounds.Add(inbound2);
} }
if (_config.Inbound.First().AllowLANConn) if (_config.Inbound.First().AllowLANConn)
{ {
if (_config.Inbound.First().NewPort4LAN) if (_config.Inbound.First().NewPort4LAN)
{ {
var inbound3 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks3, true); var inbound3 = BuildInbound(_config.Inbound.First(), EInboundProtocol.socks3, true);
inbound3.listen = listen; inbound3.listen = listen;
v2rayConfig.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> { new AccountsItem4Ray() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } }; inbound3.settings.accounts = new List<AccountsItem4Ray> { new() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } };
} }
} }
else else
@ -43,10 +43,9 @@ public partial class CoreConfigV2rayService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks) private Inbounds4Ray BuildInbound(InItem inItem, EInboundProtocol protocol, bool bSocks)
{ {
var result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound); var result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound);
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())

View file

@ -2,28 +2,27 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenLog(V2rayConfig v2rayConfig) private void GenLog()
{ {
try try
{ {
if (_config.CoreBasicItem.LogEnabled) if (_config.CoreBasicItem.LogEnabled)
{ {
var dtNow = DateTime.Now; var dtNow = DateTime.Now;
v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; _coreConfig.log.loglevel = _config.CoreBasicItem.Loglevel;
v2rayConfig.log.access = Utils.GetLogPath($"Vaccess_{dtNow:yyyy-MM-dd}.txt"); _coreConfig.log.access = Utils.GetLogPath($"Vaccess_{dtNow:yyyy-MM-dd}.txt");
v2rayConfig.log.error = Utils.GetLogPath($"Verror_{dtNow:yyyy-MM-dd}.txt"); _coreConfig.log.error = Utils.GetLogPath($"Verror_{dtNow:yyyy-MM-dd}.txt");
} }
else else
{ {
v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; _coreConfig.log.loglevel = _config.CoreBasicItem.Loglevel;
v2rayConfig.log.access = null; _coreConfig.log.access = null;
v2rayConfig.log.error = null; _coreConfig.log.error = null;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
} }

View file

@ -2,13 +2,96 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenOutbound(ProfileItem node, Outbounds4Ray outbound) private void GenOutbounds()
{
var proxyOutboundList = BuildAllProxyOutbounds();
_coreConfig.outbounds.InsertRange(0, proxyOutboundList);
if (proxyOutboundList.Count(n => n.tag.StartsWith(Global.ProxyTag)) > 1)
{
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
GenObservatory(multipleLoad);
GenBalancer(multipleLoad);
}
}
private List<Outbounds4Ray> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag)
{
var proxyOutboundList = new List<Outbounds4Ray>();
if (_node.ConfigType.IsGroupType())
{
proxyOutboundList.AddRange(BuildGroupProxyOutbounds(baseTagName));
}
else
{
proxyOutboundList.Add(BuildProxyOutbound(baseTagName));
}
if (_config.CoreBasicItem.EnableFragment)
{
var fragmentOutbound = new Outbounds4Ray
{
protocol = "freedom",
tag = $"frag-{baseTagName}",
settings = new()
{
fragment = new()
{
packets = _config.Fragment4RayItem?.Packets,
length = _config.Fragment4RayItem?.Length,
interval = _config.Fragment4RayItem?.Interval
}
}
};
var actOutboundWithTlsList =
proxyOutboundList.Where(n => n.streamSettings?.security.IsNullOrEmpty() == false
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)).ToList();
if (actOutboundWithTlsList.Count > 0)
{
proxyOutboundList.Add(fragmentOutbound);
}
foreach (var outbound in actOutboundWithTlsList)
{
outbound.streamSettings.sockopt = new()
{
dialerProxy = fragmentOutbound.tag
};
}
}
return proxyOutboundList;
}
private List<Outbounds4Ray> BuildGroupProxyOutbounds(string baseTagName = Global.ProxyTag)
{
var proxyOutboundList = new List<Outbounds4Ray>();
switch (_node.ConfigType)
{
case EConfigType.PolicyGroup:
proxyOutboundList.AddRange(BuildOutboundsList(baseTagName));
break;
case EConfigType.ProxyChain:
proxyOutboundList.AddRange(BuildChainOutboundsList(baseTagName));
break;
}
return proxyOutboundList;
}
private Outbounds4Ray BuildProxyOutbound(string baseTagName = Global.ProxyTag)
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
FillOutbound(outbound);
outbound.tag = baseTagName;
return outbound;
}
private void FillOutbound(Outbounds4Ray outbound)
{ {
try try
{ {
var protocolExtra = node.GetProtocolExtra(); var protocolExtra = _node.GetProtocolExtra();
var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
switch (node.ConfigType) switch (_node.ConfigType)
{ {
case EConfigType.VMess: case EConfigType.VMess:
{ {
@ -22,8 +105,8 @@ public partial class CoreConfigV2rayService
{ {
vnextItem = outbound.settings.vnext.First(); vnextItem = outbound.settings.vnext.First();
} }
vnextItem.address = node.Address; vnextItem.address = _node.Address;
vnextItem.port = node.Port; vnextItem.port = _node.Port;
UsersItem4Ray usersItem; UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0) if (vnextItem.users.Count <= 0)
@ -36,7 +119,7 @@ public partial class CoreConfigV2rayService
usersItem = vnextItem.users.First(); usersItem = vnextItem.users.First();
} }
usersItem.id = node.Password; usersItem.id = _node.Password;
usersItem.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0; usersItem.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
usersItem.email = Global.UserEMail; usersItem.email = Global.UserEMail;
if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity)) if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity))
@ -48,7 +131,7 @@ public partial class CoreConfigV2rayService
usersItem.security = Global.DefaultSecurity; usersItem.security = Global.DefaultSecurity;
} }
await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); FillOutboundMux(outbound, muxEnabled, muxEnabled);
outbound.settings.servers = null; outbound.settings.servers = null;
break; break;
@ -65,16 +148,16 @@ public partial class CoreConfigV2rayService
{ {
serversItem = outbound.settings.servers.First(); serversItem = outbound.settings.servers.First();
} }
serversItem.address = node.Address; serversItem.address = _node.Address;
serversItem.port = node.Port; serversItem.port = _node.Port;
serversItem.password = node.Password; serversItem.password = _node.Password;
serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(protocolExtra.SsMethod) serversItem.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
? protocolExtra.SsMethod : "none"; ? protocolExtra.SsMethod : "none";
serversItem.ota = false; serversItem.ota = false;
serversItem.level = 1; serversItem.level = 1;
await GenOutboundMux(node, outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null; outbound.settings.vnext = null;
break; break;
@ -92,25 +175,25 @@ public partial class CoreConfigV2rayService
{ {
serversItem = outbound.settings.servers.First(); serversItem = outbound.settings.servers.First();
} }
serversItem.address = node.Address; serversItem.address = _node.Address;
serversItem.port = node.Port; serversItem.port = _node.Port;
serversItem.method = null; serversItem.method = null;
serversItem.password = null; serversItem.password = null;
if (node.Username.IsNotEmpty() if (_node.Username.IsNotEmpty()
&& node.Password.IsNotEmpty()) && _node.Password.IsNotEmpty())
{ {
SocksUsersItem4Ray socksUsersItem = new() SocksUsersItem4Ray socksUsersItem = new()
{ {
user = node.Username ?? "", user = _node.Username ?? "",
pass = node.Password, pass = _node.Password,
level = 1 level = 1
}; };
serversItem.users = new List<SocksUsersItem4Ray>() { socksUsersItem }; serversItem.users = new List<SocksUsersItem4Ray>() { socksUsersItem };
} }
await GenOutboundMux(node, outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null; outbound.settings.vnext = null;
break; break;
@ -127,8 +210,8 @@ public partial class CoreConfigV2rayService
{ {
vnextItem = outbound.settings.vnext.First(); vnextItem = outbound.settings.vnext.First();
} }
vnextItem.address = node.Address; vnextItem.address = _node.Address;
vnextItem.port = node.Port; vnextItem.port = _node.Port;
UsersItem4Ray usersItem; UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0) if (vnextItem.users.Count <= 0)
@ -140,7 +223,7 @@ public partial class CoreConfigV2rayService
{ {
usersItem = vnextItem.users.First(); usersItem = vnextItem.users.First();
} }
usersItem.id = node.Password; usersItem.id = _node.Password;
usersItem.email = Global.UserEMail; usersItem.email = Global.UserEMail;
usersItem.encryption = protocolExtra.VlessEncryption; usersItem.encryption = protocolExtra.VlessEncryption;
@ -150,7 +233,7 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
await GenOutboundMux(node, outbound, false, muxEnabled); FillOutboundMux(outbound, false, muxEnabled);
} }
outbound.settings.servers = null; outbound.settings.servers = null;
break; break;
@ -167,14 +250,14 @@ public partial class CoreConfigV2rayService
{ {
serversItem = outbound.settings.servers.First(); serversItem = outbound.settings.servers.First();
} }
serversItem.address = node.Address; serversItem.address = _node.Address;
serversItem.port = node.Port; serversItem.port = _node.Port;
serversItem.password = node.Password; serversItem.password = _node.Password;
serversItem.ota = false; serversItem.ota = false;
serversItem.level = 1; serversItem.level = 1;
await GenOutboundMux(node, outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null; outbound.settings.vnext = null;
break; break;
@ -184,8 +267,8 @@ public partial class CoreConfigV2rayService
outbound.settings = new() outbound.settings = new()
{ {
version = 2, version = 2,
address = node.Address, address = _node.Address,
port = node.Port, port = _node.Port,
}; };
outbound.settings.vnext = null; outbound.settings.vnext = null;
outbound.settings.servers = null; outbound.settings.servers = null;
@ -193,7 +276,7 @@ public partial class CoreConfigV2rayService
} }
case EConfigType.WireGuard: case EConfigType.WireGuard:
{ {
var address = node.Address; var address = _node.Address;
if (Utils.IsIpv6(address)) if (Utils.IsIpv6(address))
{ {
address = $"[{address}]"; address = $"[{address}]";
@ -201,12 +284,12 @@ public partial class CoreConfigV2rayService
var peer = new WireguardPeer4Ray var peer = new WireguardPeer4Ray
{ {
publicKey = protocolExtra.WgPublicKey ?? "", publicKey = protocolExtra.WgPublicKey ?? "",
endpoint = address + ":" + node.Port.ToString() endpoint = address + ":" + _node.Port.ToString()
}; };
var setting = new Outboundsettings4Ray var setting = new Outboundsettings4Ray
{ {
address = Utils.String2List(protocolExtra.WgInterfaceAddress), address = Utils.String2List(protocolExtra.WgInterfaceAddress),
secretKey = node.Password, secretKey = _node.Password,
reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(), reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(),
mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(), mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(),
peers = [peer] peers = [peer]
@ -218,21 +301,20 @@ public partial class CoreConfigV2rayService
} }
} }
outbound.protocol = Global.ProtocolTypes[node.ConfigType]; outbound.protocol = Global.ProtocolTypes[_node.ConfigType];
if (node.ConfigType == EConfigType.Hysteria2) if (_node.ConfigType == EConfigType.Hysteria2)
{ {
outbound.protocol = "hysteria"; outbound.protocol = "hysteria";
} }
await GenBoundStreamSettings(node, outbound); FillBoundStreamSettings(outbound);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private async Task<int> GenOutboundMux(ProfileItem node, Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false) private void FillOutboundMux(Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false)
{ {
try try
{ {
@ -255,48 +337,40 @@ public partial class CoreConfigV2rayService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
private async Task<int> GenBoundStreamSettings(ProfileItem node, Outbounds4Ray outbound) private void FillBoundStreamSettings(Outbounds4Ray outbound)
{ {
try try
{ {
var streamSettings = outbound.streamSettings; var streamSettings = outbound.streamSettings;
var network = node.GetNetwork(); var network = _node.GetNetwork();
if (node.ConfigType == EConfigType.Hysteria2) if (_node.ConfigType == EConfigType.Hysteria2)
{ {
network = "hysteria"; network = "hysteria";
} }
streamSettings.network = network; streamSettings.network = network;
var host = node.RequestHost.TrimEx(); var host = _node.RequestHost.TrimEx();
var path = node.Path.TrimEx(); var path = _node.Path.TrimEx();
var sni = node.Sni.TrimEx(); var sni = _node.Sni.TrimEx();
var useragent = ""; var useragent = "";
if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty())
{ {
try useragent = Global.UserAgentTexts.GetValueOrDefault(_config.CoreBasicItem.DefUserAgent, _config.CoreBasicItem.DefUserAgent);
{
useragent = Global.UserAgentTexts[_config.CoreBasicItem.DefUserAgent];
}
catch (KeyNotFoundException)
{
useragent = _config.CoreBasicItem.DefUserAgent;
}
} }
//if tls //if tls
if (node.StreamSecurity == Global.StreamSecurity) if (_node.StreamSecurity == Global.StreamSecurity)
{ {
streamSettings.security = node.StreamSecurity; streamSettings.security = _node.StreamSecurity;
TlsSettings4Ray tlsSettings = new() TlsSettings4Ray tlsSettings = new()
{ {
allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), allowInsecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
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(),
echForceQuery = node.EchForceQuery.NullIfEmpty() echForceQuery = _node.EchForceQuery.NullIfEmpty()
}; };
if (sni.IsNotEmpty()) if (sni.IsNotEmpty())
{ {
@ -306,7 +380,7 @@ public partial class CoreConfigV2rayService
{ {
tlsSettings.serverName = Utils.String2List(host)?.First(); tlsSettings.serverName = Utils.String2List(host)?.First();
} }
var certs = CertPemManager.ParsePemChain(node.Cert); var certs = CertPemManager.ParsePemChain(_node.Cert);
if (certs.Count > 0) if (certs.Count > 0)
{ {
var certsettings = new List<CertificateSettings4Ray>(); var certsettings = new List<CertificateSettings4Ray>();
@ -323,27 +397,27 @@ public partial class CoreConfigV2rayService
tlsSettings.disableSystemRoot = true; tlsSettings.disableSystemRoot = true;
tlsSettings.allowInsecure = false; tlsSettings.allowInsecure = false;
} }
else if (!node.CertSha.IsNullOrEmpty()) else if (!_node.CertSha.IsNullOrEmpty())
{ {
tlsSettings.pinnedPeerCertSha256 = node.CertSha; tlsSettings.pinnedPeerCertSha256 = _node.CertSha;
tlsSettings.allowInsecure = false; tlsSettings.allowInsecure = false;
} }
streamSettings.tlsSettings = tlsSettings; streamSettings.tlsSettings = tlsSettings;
} }
//if Reality //if Reality
if (node.StreamSecurity == Global.StreamSecurityReality) if (_node.StreamSecurity == Global.StreamSecurityReality)
{ {
streamSettings.security = node.StreamSecurity; streamSettings.security = _node.StreamSecurity;
TlsSettings4Ray realitySettings = new() TlsSettings4Ray realitySettings = new()
{ {
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint, fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
serverName = sni, serverName = sni,
publicKey = node.PublicKey, publicKey = _node.PublicKey,
shortId = node.ShortId, shortId = _node.ShortId,
spiderX = node.SpiderX, spiderX = _node.SpiderX,
mldsa65Verify = node.Mldsa65Verify, mldsa65Verify = _node.Mldsa65Verify,
show = false, show = false,
}; };
@ -367,14 +441,14 @@ public partial class CoreConfigV2rayService
kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize;
kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize;
streamSettings.finalmask ??= new(); streamSettings.finalmask ??= new();
if (Global.KcpHeaderMaskMap.TryGetValue(node.HeaderType, out var header)) if (Global.KcpHeaderMaskMap.TryGetValue(_node.HeaderType, out var header))
{ {
streamSettings.finalmask.udp = streamSettings.finalmask.udp =
[ [
new Mask4Ray new Mask4Ray
{ {
type = header, type = header,
settings = node.HeaderType == "dns" && !host.IsNullOrEmpty() ? new MaskSettings4Ray { domain = host } : null settings = _node.HeaderType == "dns" && !host.IsNullOrEmpty() ? new MaskSettings4Ray { domain = host } : null
} }
]; ];
} }
@ -444,17 +518,17 @@ public partial class CoreConfigV2rayService
{ {
xhttpSettings.host = host; xhttpSettings.host = host;
} }
if (node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(node.HeaderType)) if (_node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(_node.HeaderType))
{ {
xhttpSettings.mode = node.HeaderType; xhttpSettings.mode = _node.HeaderType;
} }
if (node.Extra.IsNotEmpty()) if (_node.Extra.IsNotEmpty())
{ {
xhttpSettings.extra = JsonUtils.ParseJson(node.Extra); xhttpSettings.extra = JsonUtils.ParseJson(_node.Extra);
} }
streamSettings.xhttpSettings = xhttpSettings; streamSettings.xhttpSettings = xhttpSettings;
await GenOutboundMux(node, outbound); FillOutboundMux(outbound);
break; break;
//h2 //h2
@ -478,11 +552,11 @@ public partial class CoreConfigV2rayService
key = path, key = path,
header = new Header4Ray header = new Header4Ray
{ {
type = node.HeaderType type = _node.HeaderType
} }
}; };
streamSettings.quicSettings = quicsettings; streamSettings.quicSettings = quicsettings;
if (node.StreamSecurity == Global.StreamSecurity) if (_node.StreamSecurity == Global.StreamSecurity)
{ {
if (sni.IsNotEmpty()) if (sni.IsNotEmpty())
{ {
@ -490,7 +564,7 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
streamSettings.tlsSettings.serverName = node.Address; streamSettings.tlsSettings.serverName = _node.Address;
} }
} }
break; break;
@ -500,7 +574,7 @@ public partial class CoreConfigV2rayService
{ {
authority = host.NullIfEmpty(), authority = host.NullIfEmpty(),
serviceName = path, serviceName = path,
multiMode = node.HeaderType == Global.GrpcMultiMode, multiMode = _node.HeaderType == Global.GrpcMultiMode,
idle_timeout = _config.GrpcItem.IdleTimeout, idle_timeout = _config.GrpcItem.IdleTimeout,
health_check_timeout = _config.GrpcItem.HealthCheckTimeout, health_check_timeout = _config.GrpcItem.HealthCheckTimeout,
permit_without_stream = _config.GrpcItem.PermitWithoutStream, permit_without_stream = _config.GrpcItem.PermitWithoutStream,
@ -510,7 +584,7 @@ public partial class CoreConfigV2rayService
break; break;
case "hysteria": case "hysteria":
var protocolExtra = node.GetProtocolExtra(); var protocolExtra = _node.GetProtocolExtra();
var ports = protocolExtra?.Ports; var ports = protocolExtra?.Ports;
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0 int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su ? su
@ -529,14 +603,14 @@ public partial class CoreConfigV2rayService
{ {
udpHop = new HysteriaUdpHop4Ray udpHop = new HysteriaUdpHop4Ray
{ {
ports = ports.Replace(':', '-'), port = ports.Replace(':', '-'),
interval = hopInterval, interval = hopInterval,
}; };
} }
streamSettings.hysteriaSettings = new() streamSettings.hysteriaSettings = new()
{ {
version = 2, version = 2,
auth = node.Password, auth = _node.Password,
up = upMbps > 0 ? $"{upMbps}mbps" : null, up = upMbps > 0 ? $"{upMbps}mbps" : null,
down = downMbps > 0 ? $"{downMbps}mbps" : null, down = downMbps > 0 ? $"{downMbps}mbps" : null,
udphop = udpHop, udphop = udpHop,
@ -557,13 +631,13 @@ public partial class CoreConfigV2rayService
default: default:
//tcp //tcp
if (node.HeaderType == Global.TcpHeaderHttp) if (_node.HeaderType == Global.TcpHeaderHttp)
{ {
TcpSettings4Ray tcpSettings = new() TcpSettings4Ray tcpSettings = new()
{ {
header = new Header4Ray header = new Header4Ray
{ {
type = node.HeaderType type = _node.HeaderType
} }
}; };
@ -587,415 +661,142 @@ public partial class CoreConfigV2rayService
} }
break; break;
} }
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenGroupOutbound(ProfileItem node, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) if (!_node.Finalmask.IsNullOrEmpty())
{
try
{
if (!node.ConfigType.IsGroupType())
{ {
return -1; streamSettings.finalmask = JsonUtils.Deserialize<Finalmask4Ray>(_node.Finalmask);
}
var hasCycle = await GroupProfileManager.HasCycle(node);
if (hasCycle)
{
return -1;
}
var (childProfiles, profileExtraItem) = await GroupProfileManager.GetChildProfileItems(node);
if (childProfiles.Count <= 0)
{
return -1;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
if (ignoreOriginChain)
{
await GenOutboundsList(childProfiles, v2rayConfig, baseTagName);
}
else
{
await GenOutboundsListWithChain(childProfiles, v2rayConfig, baseTagName);
}
break;
case EConfigType.ProxyChain:
await GenChainOutboundsList(childProfiles, v2rayConfig, baseTagName);
break;
default:
break;
}
//add balancers
if (node.ConfigType == EConfigType.PolicyGroup)
{
var multipleLoad = profileExtraItem?.MultipleLoad ?? EMultipleLoad.LeastPing;
await GenObservatory(v2rayConfig, multipleLoad, baseTagName);
await GenBalancer(v2rayConfig, multipleLoad, baseTagName);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
private async Task<int> GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) private List<Outbounds4Ray> BuildOutboundsList(string baseTagName = Global.ProxyTag)
{ {
//fragment proxy var nodes = new List<ProfileItem>();
if (_config.CoreBasicItem.EnableFragment foreach (var nodeId in Utils.String2List(_node.GetProtocolExtra().ChildItems) ?? [])
&& v2rayConfig.outbounds.First().streamSettings?.security.IsNullOrEmpty() == false)
{ {
var fragmentOutbound = new Outbounds4Ray if (context.AllProxiesMap.TryGetValue(nodeId, out var node))
{ {
protocol = "freedom", nodes.Add(node);
tag = $"frag-{Global.ProxyTag}",
settings = new()
{
fragment = new()
{
packets = _config.Fragment4RayItem?.Packets,
length = _config.Fragment4RayItem?.Length,
interval = _config.Fragment4RayItem?.Interval
}
}
};
v2rayConfig.outbounds.Add(fragmentOutbound);
v2rayConfig.outbounds.First().streamSettings.sockopt = new()
{
dialerProxy = fragmentOutbound.tag
};
return 0;
}
if (node.Subid.IsNullOrEmpty())
{
return 0;
}
try
{
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem is null)
{
return 0;
}
//current proxy
var outbound = v2rayConfig.outbounds.First();
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
//Previous proxy
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
string? prevOutboundTag = null;
if (prevNode is not null
&& Global.XraySupportConfigType.Contains(prevNode.ConfigType))
{
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevOutboundTag = $"prev-{Global.ProxyTag}";
prevOutbound.tag = prevOutboundTag;
v2rayConfig.outbounds.Add(prevOutbound);
}
var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
if (nextOutbound is not null)
{
v2rayConfig.outbounds.Insert(0, nextOutbound);
} }
} }
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenOutboundsListWithChain(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{
try
{
// Get template and initialize list
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
return 0;
}
var resultOutbounds = new List<Outbounds4Ray>();
var prevOutbounds = new List<Outbounds4Ray>(); // Separate list for prev outbounds and fragment
// Cache for chain proxies to avoid duplicate generation
var nextProxyCache = new Dictionary<string, Outbounds4Ray?>();
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
var prevIndex = 0; // Index for prev outbounds
// Process nodes
var index = 0;
foreach (var node in nodes)
{
index++;
if (node.ConfigType.IsGroupType())
{
var (childProfiles, _) = await GroupProfileManager.GetChildProfileItems(node);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{index}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue;
}
// Handle proxy chain
string? prevTag = null;
var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null);
if (nextOutbound != null)
{
nextOutbound = JsonUtils.DeepCopy(nextOutbound);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
// current proxy
await GenOutbound(node, currentOutbound);
currentOutbound.tag = $"{baseTagName}-{index}";
if (!node.Subid.IsNullOrEmpty())
{
if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
prevTag = value; // maybe null
}
else
{
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& Global.XraySupportConfigType.Contains(prevNode.ConfigType))
{
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{baseTagName}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
prevProxyTags[node.Subid] = prevTag;
}
nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound);
if (!nextProxyCache.ContainsKey(node.Subid))
{
nextProxyCache[node.Subid] = nextOutbound;
}
}
if (nextOutbound is not null)
{
resultOutbounds.Add(nextOutbound);
}
resultOutbounds.Add(currentOutbound);
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(prevOutbounds);
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
/// <summary>
/// Generates a chained outbound configuration for the given subItem and outbound.
/// The outbound's tag must be set before calling this method.
/// Returns the next proxy's outbound configuration, which may be null if no next proxy exists.
/// </summary>
/// <param name="subItem">The subscription item containing proxy chain information.</param>
/// <param name="outbound">The current outbound configuration. Its tag must be set before calling this method.</param>
/// <param name="prevOutboundTag">The tag of the previous outbound in the chain, if any.</param>
/// <param name="nextOutbound">The outbound for the next proxy in the chain, if already created. If null, will be created inside.</param>
/// <returns>
/// The outbound configuration for the next proxy in the chain, or null if no next proxy exists.
/// </returns>
private async Task<Outbounds4Ray?> GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null)
{
try
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (!prevOutboundTag.IsNullOrEmpty())
{
outbound.streamSettings.sockopt = new()
{
dialerProxy = prevOutboundTag
};
}
// Next proxy
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& Global.XraySupportConfigType.Contains(nextNode.ConfigType))
{
if (nextOutbound == null)
{
nextOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
}
nextOutbound.tag = outbound.tag;
outbound.tag = $"mid-{outbound.tag}";
nextOutbound.streamSettings.sockopt = new()
{
dialerProxy = outbound.tag
};
}
return nextOutbound;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return null;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{
var resultOutbounds = new List<Outbounds4Ray>(); var resultOutbounds = new List<Outbounds4Ray>();
for (var i = 0; i < nodes.Count; i++) for (var i = 0; i < nodes.Count; i++)
{ {
var node = nodes[i]; var node = nodes[i];
if (node == null) var currentTag = $"{baseTagName}-{i + 1}";
{
continue;
}
if (node.ConfigType.IsGroupType()) if (node.ConfigType.IsGroupType())
{ {
var (childProfiles, _) = await GroupProfileManager.GetChildProfileItems(node); var childProfiles = new CoreConfigV2rayService(context with { Node = node, }).BuildGroupProxyOutbounds(currentTag);
if (childProfiles.Count <= 0) resultOutbounds.AddRange(childProfiles);
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue; continue;
} }
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var outbound = new CoreConfigV2rayService(context with { Node = node, }).BuildProxyOutbound();
if (txtOutbound.IsNullOrEmpty()) outbound.tag = currentTag;
{
break;
}
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var result = await GenOutbound(node, outbound);
if (result != 0)
{
break;
}
outbound.tag = baseTagName + (i + 1).ToString();
resultOutbounds.Add(outbound); resultOutbounds.Add(outbound);
} }
if (baseTagName == Global.ProxyTag) return resultOutbounds;
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0);
} }
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) private List<Outbounds4Ray> BuildChainOutboundsList(string baseTagName = Global.ProxyTag)
{ {
var nodes = new List<ProfileItem>();
foreach (var nodeId in Utils.String2List(_node.GetProtocolExtra().ChildItems) ?? [])
{
if (context.AllProxiesMap.TryGetValue(nodeId, out var node))
{
nodes.Add(node);
}
}
// Based on actual network flow instead of data packets // Based on actual network flow instead of data packets
var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
var resultOutbounds = new List<Outbounds4Ray>(); var resultOutbounds = new List<Outbounds4Ray>();
for (var i = 0; i < nodesReverse.Count; i++) for (var i = 0; i < nodesReverse.Count; i++)
{ {
var node = nodesReverse[i]; var node = nodesReverse[i];
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var currentTag = i == 0 ? baseTagName : $"chain-{baseTagName}-{i}";
if (txtOutbound.IsNullOrEmpty()) var dialerProxyTag = i != nodesReverse.Count - 1 ? $"chain-{baseTagName}-{i + 1}" : null;
if (node.ConfigType.IsGroupType())
{ {
break; var childProfiles = new CoreConfigV2rayService(context with { Node = node, }).BuildGroupProxyOutbounds(currentTag);
if (!dialerProxyTag.IsNullOrEmpty())
{
var chainEndNodes =
childProfiles.Where(n => n?.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true);
foreach (var chainEndNode in chainEndNodes)
{
chainEndNode.streamSettings.sockopt = new()
{
dialerProxy = dialerProxyTag
};
}
}
if (i != 0)
{
var chainStartNodes = childProfiles.Where(n => n.tag.StartsWith(currentTag)).ToList();
if (chainStartNodes.Count == 1)
{
foreach (var existedChainEndNode in resultOutbounds.Where(n => n.streamSettings?.sockopt?.dialerProxy == currentTag))
{
existedChainEndNode.streamSettings.sockopt = new()
{
dialerProxy = chainStartNodes.First().tag
};
}
}
else if (chainStartNodes.Count > 1)
{
var existedChainNodes = JsonUtils.DeepCopy(resultOutbounds);
resultOutbounds.Clear();
var j = 0;
foreach (var chainStartNode in chainStartNodes)
{
var existedChainNodesClone = JsonUtils.DeepCopy(existedChainNodes);
foreach (var existedChainNode in existedChainNodesClone)
{
var cloneTag = $"{existedChainNode.tag}-clone-{j + 1}";
existedChainNode.tag = cloneTag;
}
for (var k = 0; k < existedChainNodesClone.Count; k++)
{
var existedChainNode = existedChainNodesClone[k];
var previousDialerProxyTag = existedChainNode.streamSettings?.sockopt?.dialerProxy;
var nextTag = k + 1 < existedChainNodesClone.Count
? existedChainNodesClone[k + 1].tag
: chainStartNode.tag;
existedChainNode.streamSettings.sockopt = new()
{
dialerProxy = (previousDialerProxyTag == currentTag)
? chainStartNode.tag
: nextTag
};
resultOutbounds.Add(existedChainNode);
}
j++;
}
}
}
resultOutbounds.AddRange(childProfiles);
continue;
} }
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var outbound = new CoreConfigV2rayService(context with { Node = node, }).BuildProxyOutbound();
var result = await GenOutbound(node, outbound);
if (result != 0) outbound.tag = currentTag;
{
break;
}
if (i == 0) if (!dialerProxyTag.IsNullOrEmpty())
{
outbound.tag = baseTagName;
}
else
{
// avoid v2ray observe
outbound.tag = "chain-" + baseTagName + i.ToString();
}
if (i != nodesReverse.Count - 1)
{ {
outbound.streamSettings.sockopt = new() outbound.streamSettings.sockopt = new()
{ {
dialerProxy = "chain-" + baseTagName + (i + 1).ToString() dialerProxy = dialerProxyTag
}; };
} }
resultOutbounds.Add(outbound); resultOutbounds.Add(outbound);
} }
if (baseTagName == Global.ProxyTag) return resultOutbounds;
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0);
} }
} }

View file

@ -2,20 +2,20 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenRouting(V2rayConfig v2rayConfig) private void GenRouting()
{ {
try try
{ {
if (v2rayConfig.routing?.rules != null) if (_coreConfig.routing?.rules != null)
{ {
v2rayConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy; _coreConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy;
var routing = await ConfigHandler.GetDefaultRouting(_config); var routing = context.RoutingItem;
if (routing != null) if (routing != null)
{ {
if (routing.DomainStrategy.IsNotEmpty()) if (routing.DomainStrategy.IsNotEmpty())
{ {
v2rayConfig.routing.domainStrategy = routing.DomainStrategy; _coreConfig.routing.domainStrategy = routing.DomainStrategy;
} }
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet); var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item in rules) foreach (var item in rules)
@ -31,7 +31,18 @@ public partial class CoreConfigV2rayService
} }
var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item)); var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item));
await GenRoutingUserRule(item2, v2rayConfig); GenRoutingUserRule(item2);
}
}
var balancerTagList = _coreConfig.routing.balancers
?.Select(p => p.tag)
.ToList() ?? [];
if (balancerTagList.Count > 0)
{
foreach (var rulesItem in _coreConfig.routing.rules.Where(r => balancerTagList.Contains(r.outboundTag + Global.BalancerTagSuffix)))
{
rulesItem.balancerTag = rulesItem.outboundTag + Global.BalancerTagSuffix;
rulesItem.outboundTag = null;
} }
} }
} }
@ -40,95 +51,94 @@ public partial class CoreConfigV2rayService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return 0;
} }
private async Task<int> GenRoutingUserRule(RulesItem4Ray? rule, V2rayConfig v2rayConfig) private void GenRoutingUserRule(RulesItem4Ray? userRule)
{ {
try try
{ {
if (rule == null) if (userRule == null)
{ {
return 0; return;
} }
rule.outboundTag = await GenRoutingUserRuleOutbound(rule.outboundTag, v2rayConfig); userRule.outboundTag = GenRoutingUserRuleOutbound(userRule.outboundTag ?? Global.ProxyTag);
if (rule.port.IsNullOrEmpty()) if (userRule.port.IsNullOrEmpty())
{ {
rule.port = null; userRule.port = null;
} }
if (rule.network.IsNullOrEmpty()) if (userRule.network.IsNullOrEmpty())
{ {
rule.network = null; userRule.network = null;
} }
if (rule.domain?.Count == 0) if (userRule.domain?.Count == 0)
{ {
rule.domain = null; userRule.domain = null;
} }
if (rule.ip?.Count == 0) if (userRule.ip?.Count == 0)
{ {
rule.ip = null; userRule.ip = null;
} }
if (rule.protocol?.Count == 0) if (userRule.protocol?.Count == 0)
{ {
rule.protocol = null; userRule.protocol = null;
} }
if (rule.inboundTag?.Count == 0) if (userRule.inboundTag?.Count == 0)
{ {
rule.inboundTag = null; userRule.inboundTag = null;
} }
if (rule.process?.Count == 0) if (userRule.process?.Count == 0)
{ {
rule.process = null; userRule.process = null;
} }
var hasDomainIp = false; var hasDomainIp = false;
if (rule.domain?.Count > 0) if (userRule.domain?.Count > 0)
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(userRule);
it.ip = null; it.ip = null;
it.process = null; it.process = null;
it.type = "field"; it.type = "field";
for (var k = it.domain.Count - 1; k >= 0; k--) for (var k = it.domain.Count - 1; k >= 0; k--)
{ {
if (it.domain[k].StartsWith("#")) if (it.domain[k].StartsWith('#'))
{ {
it.domain.RemoveAt(k); it.domain.RemoveAt(k);
} }
it.domain[k] = it.domain[k].Replace(Global.RoutingRuleComma, ","); it.domain[k] = it.domain[k].Replace(Global.RoutingRuleComma, ",");
} }
v2rayConfig.routing.rules.Add(it); _coreConfig.routing.rules.Add(it);
hasDomainIp = true; hasDomainIp = true;
} }
if (rule.ip?.Count > 0) if (userRule.ip?.Count > 0)
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(userRule);
it.domain = null; it.domain = null;
it.process = null; it.process = null;
it.type = "field"; it.type = "field";
v2rayConfig.routing.rules.Add(it); _coreConfig.routing.rules.Add(it);
hasDomainIp = true; hasDomainIp = true;
} }
if (rule.process?.Count > 0) if (userRule.process?.Count > 0)
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(userRule);
it.domain = null; it.domain = null;
it.ip = null; it.ip = null;
it.type = "field"; it.type = "field";
v2rayConfig.routing.rules.Add(it); _coreConfig.routing.rules.Add(it);
hasDomainIp = true; hasDomainIp = true;
} }
if (!hasDomainIp) if (!hasDomainIp)
{ {
if (rule.port.IsNotEmpty() if (userRule.port.IsNotEmpty()
|| rule.protocol?.Count > 0 || userRule.protocol?.Count > 0
|| rule.inboundTag?.Count > 0 || userRule.inboundTag?.Count > 0
|| rule.network != null || userRule.network != null
) )
{ {
var it = JsonUtils.DeepCopy(rule); var it = JsonUtils.DeepCopy(userRule);
it.type = "field"; it.type = "field";
v2rayConfig.routing.rules.Add(it); _coreConfig.routing.rules.Add(it);
} }
} }
} }
@ -136,17 +146,16 @@ public partial class CoreConfigV2rayService
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
return await Task.FromResult(0);
} }
private async Task<string?> GenRoutingUserRuleOutbound(string outboundTag, V2rayConfig v2rayConfig) private string GenRoutingUserRuleOutbound(string outboundTag)
{ {
if (Global.OutboundTags.Contains(outboundTag)) if (Global.OutboundTags.Contains(outboundTag))
{ {
return outboundTag; return outboundTag;
} }
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); var node = context.AllProxiesMap.GetValueOrDefault($"remark:{outboundTag}");
if (node == null if (node == null
|| (!Global.XraySupportConfigType.Contains(node.ConfigType) || (!Global.XraySupportConfigType.Contains(node.ConfigType)
@ -156,27 +165,44 @@ public partial class CoreConfigV2rayService
} }
var tag = $"{node.IndexId}-{Global.ProxyTag}"; var tag = $"{node.IndexId}-{Global.ProxyTag}";
if (v2rayConfig.outbounds.Any(p => p.tag == tag)) if (_coreConfig.outbounds.Any(p => p.tag.StartsWith(tag)))
{ {
return tag; return tag;
} }
if (node.ConfigType.IsGroupType()) var proxyOutbounds = new CoreConfigV2rayService(context with { Node = node, }).BuildAllProxyOutbounds(tag);
_coreConfig.outbounds.AddRange(proxyOutbounds);
if (proxyOutbounds.Count(n => n.tag.StartsWith(tag)) > 1)
{ {
var ret = await GenGroupOutbound(node, v2rayConfig, tag); var multipleLoad = node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
if (ret == 0) GenObservatory(multipleLoad, tag);
{ GenBalancer(multipleLoad, tag);
return tag;
}
return Global.ProxyTag;
} }
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); return tag;
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); }
await GenOutbound(node, outbound);
outbound.tag = tag;
v2rayConfig.outbounds.Add(outbound);
return outbound.tag; private RulesItem4Ray BuildFinalRule()
{
var finalRule = new RulesItem4Ray()
{
type = "field",
network = "tcp,udp",
outboundTag = Global.ProxyTag,
};
var balancer =
_coreConfig?.routing?.balancers?.FirstOrDefault(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix, null);
var domainStrategy = _coreConfig.routing?.domainStrategy ?? Global.AsIs;
if (balancer is not null)
{
finalRule.outboundTag = null;
finalRule.balancerTag = balancer.tag;
}
if (domainStrategy == Global.IPIfNonMatch)
{
finalRule.network = null;
finalRule.ip = ["0.0.0.0/0", "::/0"];
}
return finalRule;
} }
} }

View file

@ -2,7 +2,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private async Task<int> GenStatistic(V2rayConfig v2rayConfig) private void GenStatistic()
{ {
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{ {
@ -11,17 +11,17 @@ public partial class CoreConfigV2rayService
Policy4Ray policyObj = new(); Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new(); SystemPolicy4Ray policySystemSetting = new();
v2rayConfig.stats = new Stats4Ray(); _coreConfig.stats = new Stats4Ray();
apiObj.tag = tag; apiObj.tag = tag;
v2rayConfig.metrics = apiObj; _coreConfig.metrics = apiObj;
policySystemSetting.statsOutboundDownlink = true; policySystemSetting.statsOutboundDownlink = true;
policySystemSetting.statsOutboundUplink = true; policySystemSetting.statsOutboundUplink = true;
policyObj.system = policySystemSetting; policyObj.system = policySystemSetting;
v2rayConfig.policy = policyObj; _coreConfig.policy = policyObj;
if (!v2rayConfig.inbounds.Exists(item => item.tag == tag)) if (!_coreConfig.inbounds.Exists(item => item.tag == tag))
{ {
Inbounds4Ray apiInbound = new(); Inbounds4Ray apiInbound = new();
Inboundsettings4Ray apiInboundSettings = new(); Inboundsettings4Ray apiInboundSettings = new();
@ -31,10 +31,10 @@ public partial class CoreConfigV2rayService
apiInbound.protocol = Global.InboundAPIProtocol; apiInbound.protocol = Global.InboundAPIProtocol;
apiInboundSettings.address = Global.Loopback; apiInboundSettings.address = Global.Loopback;
apiInbound.settings = apiInboundSettings; apiInbound.settings = apiInboundSettings;
v2rayConfig.inbounds.Add(apiInbound); _coreConfig.inbounds.Add(apiInbound);
} }
if (!v2rayConfig.routing.rules.Exists(item => item.outboundTag == tag)) if (!_coreConfig.routing.rules.Exists(item => item.outboundTag == tag))
{ {
RulesItem4Ray apiRoutingRule = new() RulesItem4Ray apiRoutingRule = new()
{ {
@ -43,9 +43,8 @@ public partial class CoreConfigV2rayService
type = "field" type = "field"
}; };
v2rayConfig.routing.rules.Add(apiRoutingRule); _coreConfig.routing.rules.Add(apiRoutingRule);
} }
} }
return await Task.FromResult(0);
} }
} }

View file

@ -234,13 +234,6 @@ public class AddGroupServerViewModel : MyReactiveObject
SelectedSource.SetProtocolExtra(protocolExtra); SelectedSource.SetProtocolExtra(protocolExtra);
var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, protocolExtra);
if (hasCycle)
{
NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks));
return;
}
if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);

View file

@ -218,7 +218,7 @@ public class AddServerViewModel : MyReactiveObject
return; return;
} }
List<string> shaList = new(); List<string> shaList = [];
foreach (var cert in certList) foreach (var cert in certList)
{ {
var sha = CertPemManager.GetCertSha256Thumbprint(cert); var sha = CertPemManager.GetCertSha256Thumbprint(cert);
@ -228,7 +228,7 @@ public class AddServerViewModel : MyReactiveObject
} }
shaList.Add(sha); shaList.Add(sha);
} }
CertSha = string.Join('~', shaList); CertSha = string.Join(',', shaList);
} }
private async Task FetchCert() private async Task FetchCert()
@ -247,11 +247,6 @@ public class AddServerViewModel : MyReactiveObject
{ {
serverName = SelectedSource.Address; serverName = SelectedSource.Address;
} }
if (!Utils.IsDomain(serverName))
{
UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
return;
}
if (SelectedSource.Port > 0) if (SelectedSource.Port > 0)
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
@ -277,11 +272,6 @@ public class AddServerViewModel : MyReactiveObject
{ {
serverName = SelectedSource.Address; serverName = SelectedSource.Address;
} }
if (!Utils.IsDomain(serverName))
{
UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
return;
}
if (SelectedSource.Port > 0) if (SelectedSource.Port > 0)
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";

View file

@ -540,7 +540,14 @@ public class MainWindowViewModel : MyReactiveObject
{ {
SetReloadEnabled(false); SetReloadEnabled(false);
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId); var profileItem = await ConfigHandler.GetDefaultServer(_config);
if (profileItem == null)
{
NoticeManager.Instance.Enqueue(ResUI.CheckServerSettings);
return;
}
var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, profileItem);
var msgs = new List<string>([..validatorResult.Errors, ..validatorResult.Warnings]);
if (msgs.Count > 0) if (msgs.Count > 0)
{ {
foreach (var msg in msgs) foreach (var msg in msgs)
@ -548,12 +555,15 @@ public class MainWindowViewModel : MyReactiveObject
NoticeManager.Instance.SendMessage(msg); NoticeManager.Instance.SendMessage(msg);
} }
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return; if (!validatorResult.Success)
{
return;
}
} }
await Task.Run(async () => await Task.Run(async () =>
{ {
await LoadCore(); await LoadCore(context);
await SysProxyHandler.UpdateSysProxy(_config, false); await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000); await Task.Delay(1000);
}); });
@ -594,10 +604,9 @@ public class MainWindowViewModel : MyReactiveObject
RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled);
} }
private async Task LoadCore() private async Task LoadCore(CoreConfigContext? context)
{ {
var node = await ConfigHandler.GetDefaultServer(_config); await CoreManager.Instance.LoadCore(context);
await CoreManager.Instance.LoadCore(node);
} }
#endregion core job #endregion core job

View file

@ -95,7 +95,6 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool TunStrictRoute { get; set; } [Reactive] public bool TunStrictRoute { get; set; }
[Reactive] public string TunStack { get; set; } [Reactive] public string TunStack { get; set; }
[Reactive] public int TunMtu { get; set; } [Reactive] public int TunMtu { get; set; }
[Reactive] public bool TunEnableExInbound { get; set; }
[Reactive] public bool TunEnableIPv6Address { get; set; } [Reactive] public bool TunEnableIPv6Address { get; set; }
#endregion Tun mode #endregion Tun mode
@ -220,7 +219,6 @@ public class OptionSettingViewModel : MyReactiveObject
TunStrictRoute = _config.TunModeItem.StrictRoute; TunStrictRoute = _config.TunModeItem.StrictRoute;
TunStack = _config.TunModeItem.Stack; TunStack = _config.TunModeItem.Stack;
TunMtu = _config.TunModeItem.Mtu; TunMtu = _config.TunModeItem.Mtu;
TunEnableExInbound = _config.TunModeItem.EnableExInbound;
TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address; TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address;
#endregion Tun mode #endregion Tun mode
@ -380,7 +378,6 @@ public class OptionSettingViewModel : MyReactiveObject
_config.TunModeItem.StrictRoute = TunStrictRoute; _config.TunModeItem.StrictRoute = TunStrictRoute;
_config.TunModeItem.Stack = TunStack; _config.TunModeItem.Stack = TunStack;
_config.TunModeItem.Mtu = TunMtu; _config.TunModeItem.Mtu = TunMtu;
_config.TunModeItem.EnableExInbound = TunEnableExInbound;
_config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address; _config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address;
//coreType //coreType

View file

@ -788,7 +788,8 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
var msgs = await ActionPrecheckManager.Instance.Check(item); var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item);
var msgs = new List<string>([..validatorResult.Errors, ..validatorResult.Warnings]);
if (msgs.Count > 0) if (msgs.Count > 0)
{ {
foreach (var msg in msgs) foreach (var msg in msgs)
@ -796,12 +797,15 @@ public class ProfilesViewModel : MyReactiveObject
NoticeManager.Instance.SendMessage(msg); NoticeManager.Instance.SendMessage(msg);
} }
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return; if (!validatorResult.Success)
{
return;
}
} }
if (blClipboard) if (blClipboard)
{ {
var result = await CoreConfigHandler.GenerateClientConfig(item, null); var result = await CoreConfigHandler.GenerateClientConfig(context, null);
if (result.Success != true) if (result.Success != true)
{ {
NoticeManager.Instance.Enqueue(result.Msg); NoticeManager.Instance.Enqueue(result.Msg);
@ -824,7 +828,21 @@ public class ProfilesViewModel : MyReactiveObject
{ {
return; return;
} }
var result = await CoreConfigHandler.GenerateClientConfig(item, fileName); var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item);
var msgs = new List<string>([..validatorResult.Errors, ..validatorResult.Warnings]);
if (msgs.Count > 0)
{
foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
if (!validatorResult.Success)
{
return;
}
}
var result = await CoreConfigHandler.GenerateClientConfig(context, fileName);
if (result.Success != true) if (result.Success != true)
{ {
NoticeManager.Instance.Enqueue(result.Msg); NoticeManager.Instance.Enqueue(result.Msg);

View file

@ -32,7 +32,7 @@
IsCancel="True" /> IsCancel="True" />
</StackPanel> </StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid <Grid
Grid.Row="0" Grid.Row="0"
@ -407,7 +407,7 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPorts7Tips}" /> Text="{x:Static resx:ResUI.TbPorts7Tips}" />
<TextBlock <TextBlock
Grid.Row="4" Grid.Row="4"
Grid.Column="0" Grid.Column="0"
@ -420,7 +420,7 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
<TextBlock <TextBlock
Grid.Row="5" Grid.Row="5"
Grid.Column="0" Grid.Column="0"
@ -722,11 +722,51 @@
Text="{x:Static resx:ResUI.TbPath}" /> Text="{x:Static resx:ResUI.TbPath}" />
</Grid> </Grid>
<Separator Grid.Row="5" Margin="{StaticResource MarginTb8}" /> <Grid
x:Name="gridFinalmask"
Grid.Row="5"
ColumnDefinitions="300,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbFinalmask}" />
<Button
Grid.Row="0"
Grid.Column="1"
Margin="{StaticResource MarginLr4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Classes="IconButton">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconMore}">
<PathIcon.RenderTransform>
<RotateTransform Angle="90" />
</PathIcon.RenderTransform>
</PathIcon>
</Button.Content>
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBox
x:Name="txtFinalmask"
Width="400"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
Classes="TextArea"
TextWrapping="NoWrap" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<Separator Grid.Row="6" Margin="{StaticResource MarginTb8}" />
<Grid <Grid
x:Name="gridTls" x:Name="gridTls"
Grid.Row="6" Grid.Row="7"
ColumnDefinitions="300,Auto" ColumnDefinitions="300,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto">
@ -745,7 +785,7 @@
</Grid> </Grid>
<Grid <Grid
x:Name="gridTlsMore" x:Name="gridTlsMore"
Grid.Row="7" Grid.Row="8"
ColumnDefinitions="300,Auto" ColumnDefinitions="300,Auto"
IsVisible="False" IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
@ -908,7 +948,7 @@
</Grid> </Grid>
<Grid <Grid
x:Name="gridRealityMore" x:Name="gridRealityMore"
Grid.Row="7" Grid.Row="8"
ColumnDefinitions="300,Auto" ColumnDefinitions="300,Auto"
IsVisible="False" IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
@ -996,7 +1036,7 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
</Grid> </Grid>
<Separator Grid.Row="8" Margin="{StaticResource MarginTb8}" /> <Separator Grid.Row="9" Margin="{StaticResource MarginTb8}" />
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</DockPanel> </DockPanel>

View file

@ -78,6 +78,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
cmbCoreType.IsEnabled = false; cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false; cmbFingerprint.IsEnabled = false;
cmbFingerprint.SelectedValue = string.Empty; cmbFingerprint.SelectedValue = string.Empty;
gridFinalmask.IsVisible = false;
cmbHeaderType8.ItemsSource = Global.TuicCongestionControls; cmbHeaderType8.ItemsSource = Global.TuicCongestionControls;
break; break;
@ -95,6 +96,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
gridAnytls.IsVisible = true; gridAnytls.IsVisible = true;
lstStreamSecurity.Add(Global.StreamSecurityReality); lstStreamSecurity.Add(Global.StreamSecurityReality);
cmbCoreType.IsEnabled = false; cmbCoreType.IsEnabled = false;
gridFinalmask.IsVisible = false;
break; break;
} }
cmbStreamSecurity.ItemsSource = lstStreamSecurity; cmbStreamSecurity.ItemsSource = lstStreamSecurity;
@ -194,6 +196,8 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Finalmask, v => v.txtFinalmask.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);

View file

@ -325,12 +325,6 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="20"
Grid.Column="2"
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.TbSettingsEnableFragmentTips}"
TextWrapping="Wrap" />
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@ -843,19 +837,6 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="6"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsEnableExInbound}" />
<ToggleSwitch
x:Name="togEnableExInbound"
Grid.Row="6"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="7" Grid.Row="7"
Grid.Column="0" Grid.Column="0"

View file

@ -114,7 +114,6 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableExInbound, v => v.togEnableExInbound.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.SelectedValue).DisposeWith(disposables);

View file

@ -60,6 +60,8 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0"> <Grid Grid.Row="0">
@ -928,12 +930,47 @@
Text="{x:Static resx:ResUI.TbPath}" /> Text="{x:Static resx:ResUI.TbPath}" />
</Grid> </Grid>
<Grid x:Name="gridFinalmask" Grid.Row="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbFinalmask}" />
<materialDesign:PopupBox
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
StaysOpen="True"
Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<StackPanel>
<TextBox
x:Name="txtFinalmask"
Width="400"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
HorizontalScrollBarVisibility="Auto"
MaxLines="8"
MinLines="4"
Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="NoWrap"
VerticalScrollBarVisibility="Auto" />
</StackPanel>
</materialDesign:PopupBox>
</Grid>
<Separator <Separator
Grid.Row="5" Grid.Row="6"
Margin="0,2" Margin="0,2"
Style="{DynamicResource MaterialDesignSeparator}" /> Style="{DynamicResource MaterialDesignSeparator}" />
<Grid x:Name="gridTls" Grid.Row="6"> <Grid x:Name="gridTls" Grid.Row="7">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -963,7 +1000,7 @@
</Grid> </Grid>
<Grid <Grid
x:Name="gridTlsMore" x:Name="gridTlsMore"
Grid.Row="7" Grid.Row="8"
Visibility="Hidden"> Visibility="Hidden">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -1152,7 +1189,7 @@
</Grid> </Grid>
<Grid <Grid
x:Name="gridRealityMore" x:Name="gridRealityMore"
Grid.Row="7" Grid.Row="8"
Visibility="Hidden"> Visibility="Hidden">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -1264,7 +1301,7 @@
Style="{StaticResource DefTextBox}" /> Style="{StaticResource DefTextBox}" />
</Grid> </Grid>
<Separator <Separator
Grid.Row="8" Grid.Row="9"
Margin="0,2" Margin="0,2"
Style="{DynamicResource MaterialDesignSeparator}" /> Style="{DynamicResource MaterialDesignSeparator}" />
</Grid> </Grid>

View file

@ -73,6 +73,7 @@ public partial class AddServerWindow
cmbCoreType.IsEnabled = false; cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false; cmbFingerprint.IsEnabled = false;
cmbFingerprint.Text = string.Empty; cmbFingerprint.Text = string.Empty;
gridFinalmask.Visibility = Visibility.Collapsed;
cmbHeaderType8.ItemsSource = Global.TuicCongestionControls; cmbHeaderType8.ItemsSource = Global.TuicCongestionControls;
break; break;
@ -90,6 +91,7 @@ public partial class AddServerWindow
gridAnytls.Visibility = Visibility.Visible; gridAnytls.Visibility = Visibility.Visible;
cmbCoreType.IsEnabled = false; cmbCoreType.IsEnabled = false;
lstStreamSecurity.Add(Global.StreamSecurityReality); lstStreamSecurity.Add(Global.StreamSecurityReality);
gridFinalmask.Visibility = Visibility.Collapsed;
break; break;
} }
cmbStreamSecurity.ItemsSource = lstStreamSecurity; cmbStreamSecurity.ItemsSource = lstStreamSecurity;
@ -190,6 +192,8 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Finalmask, v => v.txtFinalmask.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);

View file

@ -391,13 +391,6 @@
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock
Grid.Row="20"
Grid.Column="2"
Margin="{StaticResource Margin8}"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsEnableFragmentTips}"
TextWrapping="Wrap" />
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@ -1097,20 +1090,6 @@
HorizontalAlignment="Left" HorizontalAlignment="Left"
Style="{StaticResource DefComboBox}" /> Style="{StaticResource DefComboBox}" />
<TextBlock
Grid.Row="6"
Grid.Column="0"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsEnableExInbound}" />
<ToggleButton
x:Name="togEnableExInbound"
Grid.Row="6"
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="7" Grid.Row="7"
Grid.Column="0" Grid.Column="0"

View file

@ -119,7 +119,6 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableExInbound, v => v.togEnableExInbound.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.Text).DisposeWith(disposables);