sync upstream 1.14.0 and trim rr build
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
1778450375
|
||||
1788503096
|
||||
|
||||
+21
-4
@@ -8,8 +8,8 @@ GO_CACHE_DIR="${GO_CACHE_DIR:-/tmp/sing-box-rr-go-build}"
|
||||
GO_MODCACHE_DIR="${GO_MODCACHE_DIR:-/tmp/sing-box-rr-go-mod}"
|
||||
GO_PROXY_VALUE="${GOPROXY:-https://proxy.golang.org|direct}"
|
||||
LD_LLD_BIN="${LD_LLD_BIN:-}"
|
||||
SDK_ARM64="/home/fresa/sdk/openwrt-sdk-25.12.4-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64"
|
||||
SDK_X86="/home/fresa/sdk/openwrt-sdk-25.12.4-x86-64_gcc-14.3.0_musl.Linux-x86_64"
|
||||
SDK_ARM64="/home/fresa/sdk/openwrt-sdk-25.12.5-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64"
|
||||
SDK_X86="/home/fresa/sdk/openwrt-sdk-25.12.5-x86-64_gcc-14.3.0_musl.Linux-x86_64"
|
||||
PACKAGES_DIR="$SCRIPT_DIR/packages/25.12"
|
||||
PKG_NAME="sing-box-rr"
|
||||
FORCE_REBUILD=0
|
||||
@@ -250,6 +250,22 @@ build_ldflags() {
|
||||
echo "${base_ldflags} -X github.com/sagernet/sing-box/constant.Version=$(project_version) -s -w"
|
||||
}
|
||||
|
||||
go_toolchain_value() {
|
||||
local required_version
|
||||
|
||||
if [ -n "${GOTOOLCHAIN:-}" ]; then
|
||||
printf '%s\n' "$GOTOOLCHAIN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
required_version="$(sed -nE 's/^go ([0-9]+([.][0-9]+){1,2}).*$/\1/p' "$PROJECT_DIR/go.mod" | head -n1)"
|
||||
if [ -n "$required_version" ]; then
|
||||
printf 'go%s+auto\n' "$required_version"
|
||||
else
|
||||
printf 'auto\n'
|
||||
fi
|
||||
}
|
||||
|
||||
require_tools() {
|
||||
[ -x "$GO_BIN" ] || { echo "go not found at $GO_BIN" | tee -a "$LOG_FILE"; exit 1; }
|
||||
}
|
||||
@@ -264,15 +280,16 @@ reset_go_cache_if_needed() {
|
||||
|
||||
build_binary() {
|
||||
local goarch="$1" output="$2" sdk_dir="$3" toolchain_dir="$4"
|
||||
local tags ldflags
|
||||
local tags ldflags gotoolchain
|
||||
local -a env_cmd
|
||||
|
||||
tags="$(build_tags)"
|
||||
ldflags="$(build_ldflags)"
|
||||
gotoolchain="$(go_toolchain_value)"
|
||||
env_cmd=(
|
||||
env
|
||||
"PATH=$(dirname "$GO_BIN"):$PATH"
|
||||
"GOTOOLCHAIN=local"
|
||||
"GOTOOLCHAIN=$gotoolchain"
|
||||
"GOCACHE=$GO_CACHE_DIR"
|
||||
"GOMODCACHE=$GO_MODCACHE_DIR"
|
||||
"GOPROXY=$GO_PROXY_VALUE"
|
||||
|
||||
+20
-3
@@ -8,7 +8,7 @@ GO_CACHE_DIR="${GO_CACHE_DIR:-/tmp/sing-box-rr-go-build}"
|
||||
GO_MODCACHE_DIR="${GO_MODCACHE_DIR:-/tmp/sing-box-rr-go-mod}"
|
||||
GO_PROXY_VALUE="${GOPROXY:-https://proxy.golang.org|direct}"
|
||||
LD_LLD_BIN="${LD_LLD_BIN:-}"
|
||||
SDK_ARM64="/home/fresa/sdk/openwrt-sdk-24.10.4-mediatek-filogic_gcc-13.3.0_musl.Linux-x86_64"
|
||||
SDK_ARM64="/home/fresa/sdk/openwrt-sdk-24.10.8-mediatek-filogic_gcc-13.3.0_musl.Linux-x86_64"
|
||||
PACKAGES_DIR="$SCRIPT_DIR/packages/24.10"
|
||||
PKG_NAME="sing-box-rr"
|
||||
FORCE_REBUILD=0
|
||||
@@ -212,6 +212,22 @@ build_ldflags() {
|
||||
echo "${base_ldflags} -X github.com/sagernet/sing-box/constant.Version=$(project_version) -s -w"
|
||||
}
|
||||
|
||||
go_toolchain_value() {
|
||||
local required_version
|
||||
|
||||
if [ -n "${GOTOOLCHAIN:-}" ]; then
|
||||
printf '%s\n' "$GOTOOLCHAIN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
required_version="$(sed -nE 's/^go ([0-9]+([.][0-9]+){1,2}).*$/\1/p' "$PROJECT_DIR/go.mod" | head -n1)"
|
||||
if [ -n "$required_version" ]; then
|
||||
printf 'go%s+auto\n' "$required_version"
|
||||
else
|
||||
printf 'auto\n'
|
||||
fi
|
||||
}
|
||||
|
||||
require_tools() {
|
||||
[ -x "$GO_BIN" ] || { echo "go not found at $GO_BIN" | tee -a "$LOG_FILE"; exit 1; }
|
||||
command -v fakeroot >/dev/null || { echo "fakeroot not found in PATH" | tee -a "$LOG_FILE"; exit 1; }
|
||||
@@ -227,15 +243,16 @@ reset_go_cache_if_needed() {
|
||||
|
||||
build_binary() {
|
||||
local goarch="$1" output="$2" sdk_dir="$3" toolchain_dir="$4"
|
||||
local tags ldflags
|
||||
local tags ldflags gotoolchain
|
||||
local -a env_cmd
|
||||
|
||||
tags="$(build_tags)"
|
||||
ldflags="$(build_ldflags)"
|
||||
gotoolchain="$(go_toolchain_value)"
|
||||
env_cmd=(
|
||||
env
|
||||
"PATH=$(dirname "$GO_BIN"):$PATH"
|
||||
"GOTOOLCHAIN=local"
|
||||
"GOTOOLCHAIN=$gotoolchain"
|
||||
"GOCACHE=$GO_CACHE_DIR"
|
||||
"GOMODCACHE=$GO_MODCACHE_DIR"
|
||||
"GOPROXY=$GO_PROXY_VALUE"
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -1,12 +1,16 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--name sing-box-extended
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--description "The universal proxy platform (extended)."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--provides sing-box
|
||||
--conflicts sing-box
|
||||
--replaces sing-box
|
||||
|
||||
--config-files /etc/config/sing-box
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
e4926ba205fae5351e3d3eeafff7e7029654424a
|
||||
45832ab074849607406baa3e3a2c4660274602ed
|
||||
|
||||
@@ -31,6 +31,8 @@ body:
|
||||
- sing-box for macOS Graphical Client
|
||||
- sing-box for Apple tvOS Graphical Client
|
||||
- sing-box for Android Graphical Client
|
||||
- sing-box for Windows Graphical Client
|
||||
- sing-box for Linux Graphical Client
|
||||
- Third-party graphical clients that advertise themselves as using sing-box (Windows)
|
||||
- Third-party graphical clients that advertise themselves as using sing-box (Android)
|
||||
- Others
|
||||
|
||||
@@ -31,6 +31,8 @@ body:
|
||||
- sing-box for macOS 图形客户端程序
|
||||
- sing-box for Apple tvOS 图形客户端程序
|
||||
- sing-box for Android 图形客户端程序
|
||||
- sing-box for Windows 图形客户端程序
|
||||
- sing-box for Linux 图形客户端程序
|
||||
- 宣传使用 sing-box 的第三方图形客户端程序 (Windows)
|
||||
- 宣传使用 sing-box 的第三方图形客户端程序 (Android)
|
||||
- 其他
|
||||
|
||||
+5
-7
@@ -27,10 +27,7 @@ fi
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
|
||||
# Convert version to APK format:
|
||||
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
||||
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
|
||||
# 1.13.0 -> 1.13.0-r0
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/' | sed -E 's/-[a-z]+-/./g')
|
||||
APK_VERSION="${APK_VERSION}-r0"
|
||||
|
||||
ROOT_DIR=$(mktemp -d)
|
||||
@@ -78,15 +75,16 @@ done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||
|
||||
# Build APK
|
||||
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||
--info "name:sing-box" \
|
||||
--info "name:sing-box-extended" \
|
||||
--info "version:${APK_VERSION}" \
|
||||
--info "description:The universal proxy platform." \
|
||||
--info "description:The universal proxy platform (extended)." \
|
||||
--info "arch:${ARCHITECTURE}" \
|
||||
--info "license:GPL-3.0-or-later" \
|
||||
--info "origin:sing-box" \
|
||||
--info "origin:sing-box-extended" \
|
||||
--info "url:https://sing-box.sagernet.org/" \
|
||||
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
||||
--info "provides:sing-box" \
|
||||
--info "provider-priority:100" \
|
||||
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
||||
--files "$ROOT_DIR" \
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e -o pipefail
|
||||
|
||||
VERSION="$1"
|
||||
TARGET="$2"
|
||||
BINARY_PATH="$3"
|
||||
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
DIST="$PROJECT/dist"
|
||||
|
||||
case "$TARGET" in
|
||||
linux_amd64*) ARCHITECTURES="x86_64" ;;
|
||||
linux_arm64*) ARCHITECTURES="aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" ;;
|
||||
linux_386*softfloat) ARCHITECTURES="i386_pentium-mmx" ;;
|
||||
linux_386*) ARCHITECTURES="i386_pentium4" ;;
|
||||
linux_arm_7*) ARCHITECTURES="arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" ;;
|
||||
linux_arm_6*) ARCHITECTURES="arm_arm1176jzf-s_vfp" ;;
|
||||
linux_arm_5*) ARCHITECTURES="arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" ;;
|
||||
linux_mips64_*) ARCHITECTURES="mips64_mips64r2 mips64_octeonplus" ;;
|
||||
linux_mips64le*) ARCHITECTURES="mips64el_mips64r2" ;;
|
||||
linux_mipsle*hardfloat) ARCHITECTURES="mipsel_24kc_24kf" ;;
|
||||
linux_mipsle*) ARCHITECTURES="mipsel_24kc mipsel_74kc mipsel_mips32" ;;
|
||||
linux_mips_*) ARCHITECTURES="mips_24kc mips_4kec mips_mips32" ;;
|
||||
linux_riscv64*) ARCHITECTURES="riscv64_generic" ;;
|
||||
linux_loong64*) ARCHITECTURES="loongarch64_generic" ;;
|
||||
*) echo "Unknown target: $TARGET"; exit 1 ;;
|
||||
esac
|
||||
|
||||
PKG_VERSION="${VERSION//-/\~}"
|
||||
|
||||
FPM_DIR=$(mktemp -d)
|
||||
sed "s|release/|$PROJECT/release/|g;s|^LICENSE|$PROJECT/LICENSE|" "$PROJECT/.fpm_openwrt" > "$FPM_DIR/.fpm"
|
||||
trap 'rm -rf "$FPM_DIR"' EXIT
|
||||
|
||||
for ARCH in $ARCHITECTURES; do
|
||||
TMP_DEB=$(mktemp -p "$DIST" _openwrt_XXXXXX.deb)
|
||||
rm -f "$TMP_DEB"
|
||||
(cd "$FPM_DIR" && fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "$TMP_DEB" \
|
||||
--architecture all \
|
||||
"$BINARY_PATH=/usr/bin/sing-box")
|
||||
|
||||
bash "$PROJECT/.github/deb2ipk.sh" \
|
||||
"$ARCH" \
|
||||
"$TMP_DEB" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.ipk"
|
||||
rm -f "$TMP_DEB"
|
||||
|
||||
if command -v apk &>/dev/null; then
|
||||
bash "$PROJECT/.github/build_openwrt_apk.sh" \
|
||||
"$ARCH" "$VERSION" "$BINARY_PATH" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.apk"
|
||||
fi
|
||||
|
||||
echo "Built: sing-box-extended_${VERSION}_openwrt_${ARCH} (.ipk/.apk)"
|
||||
done
|
||||
+8
-6
@@ -2,10 +2,11 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.25.9"
|
||||
VERSION="1.26.7"
|
||||
PATCH_COMMITS=(
|
||||
"afe69d3cec1c6dcf0f1797b20546795730850070"
|
||||
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
|
||||
"f080b0c6346eb690c0dc82497b35925f385b35ac"
|
||||
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
|
||||
"367663849c656612fc3c8875ef6e7f6b0c93dbd0"
|
||||
)
|
||||
CURL_ARGS=(
|
||||
-fL
|
||||
@@ -25,12 +26,13 @@ tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
|
||||
mv go go_osx
|
||||
cd go_osx
|
||||
|
||||
# these patch URLs only work on golang1.25.x
|
||||
# that means after golang1.26 release it must be changed
|
||||
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
|
||||
# these patch URLs only work on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/SagerNet/go/commits/release-branch.go1.26/
|
||||
# revert:
|
||||
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
|
||||
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
|
||||
# d90a57ffe8: "cmd/link/internal/ld: unify OS/SDK versions for macOS linking"
|
||||
|
||||
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
||||
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||
|
||||
+10
-10
@@ -2,14 +2,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.25.9"
|
||||
VERSION="1.26.7"
|
||||
PATCH_COMMITS=(
|
||||
"466f6c7a29bc098b0d4c987b803c779222894a11"
|
||||
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
|
||||
"a90777dcf692dd2168577853ba743b4338721b06"
|
||||
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
|
||||
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
|
||||
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
|
||||
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
|
||||
"95b851f661584711faa8115b3234a461044f4510"
|
||||
"b0bf0a863cf218b9bb0d6c013903ab160ae0c39b"
|
||||
"7c7a2a0d68920f8764d9a3aeae7ad1067a17b3fa"
|
||||
"ba34356e82f3c12a7303d2231e6ea0e42bbbb3bf"
|
||||
"472a88edbc2a42feb53fa31d29ab54d33840dca6"
|
||||
)
|
||||
CURL_ARGS=(
|
||||
-fL
|
||||
@@ -29,9 +29,9 @@ mv go go_win7
|
||||
cd go_win7
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# these patch URLs only work on golang1.25.x
|
||||
# that means after golang1.26 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||
# these patch URLs only work on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ PROJECTS=$(dirname "$0")/../..
|
||||
|
||||
function updateClient() {
|
||||
pushd clients/$1
|
||||
git fetch
|
||||
git fetch origin main
|
||||
git reset FETCH_HEAD --hard
|
||||
popd
|
||||
git add clients/$1
|
||||
@@ -12,3 +12,4 @@ function updateClient() {
|
||||
|
||||
updateClient "apple"
|
||||
updateClient "android"
|
||||
updateClient "desktop"
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ git -C $PROJECTS/cronet-go fetch origin go_dev
|
||||
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
|
||||
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
|
||||
go mod tidy
|
||||
git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||
git -C $PROJECTS/cronet-go rev-parse origin/go_dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||
|
||||
+977
-200
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.9
|
||||
go-version: 1.26.7
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -63,7 +63,8 @@ jobs:
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
git init ~/cronet-go
|
||||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
|
||||
+42
-3
@@ -18,21 +18,60 @@ on:
|
||||
- testing
|
||||
- unstable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: Lint ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
- goos: windows
|
||||
goarch: '386'
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
- goos: linux
|
||||
goarch: '386'
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
# - goos: freebsd
|
||||
# goarch: amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25
|
||||
go-version: ^1.26
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
|
||||
+6
-6
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.9
|
||||
go-version: 1.26.7
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
name: Build binary
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-26.04
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.9
|
||||
go-version: 1.26.7
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -80,7 +80,8 @@ jobs:
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
git init ~/cronet-go
|
||||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
@@ -182,8 +183,6 @@ jobs:
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
@@ -194,6 +193,7 @@ jobs:
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
echo '%_rpmformat 4' > "$HOME/.rpmmacros"
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
--name "${NAME}" \
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/test.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
go:
|
||||
- ~1.25
|
||||
- ~1.26
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Set build tags and ldflags
|
||||
shell: bash
|
||||
run: |
|
||||
echo "BUILD_TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)" >> "$GITHUB_ENV"
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "$GITHUB_ENV"
|
||||
- name: Test (unix)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: go test -v -exec sudo -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./...
|
||||
- name: Test (windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: bash
|
||||
run: go test -v -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./...
|
||||
@@ -23,3 +23,10 @@ AGENTS.md
|
||||
/.claude/
|
||||
dist
|
||||
logs
|
||||
/*.so
|
||||
/*.log
|
||||
/*.db-shm
|
||||
/*.db-wal
|
||||
/*.db.backup.*
|
||||
/test_download.bin
|
||||
/wget-log
|
||||
+11
-19
@@ -1,6 +1,6 @@
|
||||
version: "2"
|
||||
run:
|
||||
go: "1.25"
|
||||
go: "1.24"
|
||||
build-tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
@@ -17,30 +17,29 @@ run:
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- ineffassign
|
||||
- paralleltest
|
||||
- staticcheck
|
||||
- unused
|
||||
- modernize
|
||||
settings:
|
||||
modernize:
|
||||
disable:
|
||||
- omitzero # nested struct omitempty -> omitzero changes JSON output semantics
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -S1000
|
||||
- -S1008
|
||||
- -S1017
|
||||
- -ST1003
|
||||
- -QF1001
|
||||
- -QF1003
|
||||
- -QF1008
|
||||
- -QF1008 # could remove embedded field "<interface>" from selector
|
||||
- -ST1003 # should not use ALL_CAPS in Go names; use CamelCase instead
|
||||
- -QF1001 # could apply De Morgan's law
|
||||
- -SA4023 # impossible nil comparison, false positive on platform-specific implementations
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- transport/simple-obfs
|
||||
- \.pb\.go$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
@@ -55,10 +54,3 @@ formatters:
|
||||
- prefix(github.com/sagernet/)
|
||||
- default
|
||||
custom-order: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- transport/simple-obfs
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@@ -11,6 +11,7 @@ builds:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -checklinkname=0
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
@@ -22,8 +23,14 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
@@ -54,6 +61,12 @@ builds:
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
@@ -94,6 +107,353 @@ builds:
|
||||
- android_arm64
|
||||
- android_386
|
||||
- android_amd64
|
||||
- id: naive-purego-linux-amd64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- id: naive-purego-linux-arm64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- linux_arm64
|
||||
- id: naive-purego-windows-amd64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- windows_amd64_v1
|
||||
- id: naive-purego-windows-arm64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- windows_arm64
|
||||
- id: naive-glibc
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
- CGO_LDFLAGS=-fuse-ld=lld -Wl,-z,notext -Wl,-z,execstack
|
||||
- CC_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=x86_64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_amd64_staging
|
||||
- CXX_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=x86_64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_amd64_staging
|
||||
- CC_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=aarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_arm64_staging
|
||||
- CXX_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=aarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_arm64_staging
|
||||
- CC_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=i686-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_i386_staging
|
||||
- CXX_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=i686-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_i386_staging
|
||||
- CC_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=arm-linux-gnueabihf --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_armhf_staging
|
||||
- CXX_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=arm-linux-gnueabihf --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_armhf_staging
|
||||
- CC_linux_mips64le={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=mips64el-linux-gnuabi64 --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_mips64el_staging
|
||||
- CXX_linux_mips64le={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=mips64el-linux-gnuabi64 --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_mips64el_staging
|
||||
- CC_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=riscv64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/trixie/trixie_riscv64_staging
|
||||
- CXX_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=riscv64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/trixie/trixie_riscv64_staging
|
||||
- CC_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=loongarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/sid/sid_loong64_staging
|
||||
- CXX_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=loongarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/sid/sid_loong64_staging
|
||||
- 'CC={{ index .Env (print "CC_" .Os "_" .Arch) }}'
|
||||
- 'CXX={{ index .Env (print "CXX_" .Os "_" .Arch) }}'
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: naive-musl
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_musl
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
- CGO_LDFLAGS=-fuse-ld=lld -Wl,-z,notext -Wl,-z,execstack
|
||||
- CC_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=x86_64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/x86_64
|
||||
- CXX_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=x86_64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/x86_64
|
||||
- CC_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=aarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/aarch64
|
||||
- CXX_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=aarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/aarch64
|
||||
- CC_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=i486-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/i386_pentium4
|
||||
- CXX_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=i486-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/i386_pentium4
|
||||
- CC_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=arm-openwrt-linux-musleabi --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/arm_cortex-a15_neon-vfpv4
|
||||
- CXX_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=arm-openwrt-linux-musleabi --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/arm_cortex-a15_neon-vfpv4
|
||||
- CC_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=riscv64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/riscv64
|
||||
- CXX_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=riscv64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/riscv64
|
||||
- CC_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=loongarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/24.10.5/loongarch64
|
||||
- CXX_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=loongarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/24.10.5/loongarch64
|
||||
- 'CC={{ index .Env (print "CC_" .Os "_" .Arch) }}'
|
||||
- 'CXX={{ index .Env (print "CXX_" .Os "_" .Arch) }}'
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: compressed
|
||||
<<: *template
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- linux_riscv64
|
||||
- id: compressed-mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
- linux_mipsle
|
||||
- linux_mipsle_softfloat
|
||||
- id: compressed-android
|
||||
<<: *template
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
overrides:
|
||||
- goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
env:
|
||||
- CC=armv7a-linux-androideabi21-clang
|
||||
- CXX=armv7a-linux-androideabi21-clang++
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
env:
|
||||
- CC=aarch64-linux-android21-clang
|
||||
- CXX=aarch64-linux-android21-clang++
|
||||
- goos: android
|
||||
goarch: 386
|
||||
env:
|
||||
- CC=i686-linux-android21-clang
|
||||
- CXX=i686-linux-android21-clang++
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
env:
|
||||
- CC=x86_64-linux-android21-clang
|
||||
- CXX=x86_64-linux-android21-clang++
|
||||
targets:
|
||||
- android_arm_7
|
||||
- android_arm64
|
||||
- android_386
|
||||
- android_amd64
|
||||
- id: openwrt
|
||||
<<: *template
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_arm_6
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: openwrt-mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_arm_5
|
||||
- linux_mips_softfloat
|
||||
- linux_mips64_softfloat
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mipsle_hardfloat
|
||||
- linux_mips64le
|
||||
upx:
|
||||
- enabled: true
|
||||
ids:
|
||||
- compressed
|
||||
- compressed-mips
|
||||
- compressed-android
|
||||
compress: best
|
||||
lzma: true
|
||||
archives:
|
||||
- &template
|
||||
id: archive
|
||||
@@ -111,11 +471,59 @@ archives:
|
||||
files:
|
||||
- LICENSE
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
- id: archive-legacy
|
||||
- id: archive-naive-purego-linux-amd64
|
||||
<<: *template
|
||||
builds:
|
||||
- legacy
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||
- naive-purego-linux-amd64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-linux-amd64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-purego-linux-arm64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-linux-arm64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-linux-arm64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-purego-windows-amd64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-windows-amd64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-windows-amd64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-purego-windows-arm64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-windows-arm64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-windows-arm64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-glibc
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-glibc
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}-glibc'
|
||||
- id: archive-naive-musl
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-musl
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}-musl'
|
||||
- id: archive-compressed
|
||||
<<: *template
|
||||
builds:
|
||||
- compressed
|
||||
- compressed-mips
|
||||
- compressed-android
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}-compressed'
|
||||
source:
|
||||
enabled: false
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||
@@ -134,5 +542,12 @@ release:
|
||||
mode: replace
|
||||
ids:
|
||||
- archive
|
||||
- package
|
||||
- archive-naive-glibc
|
||||
- archive-naive-musl
|
||||
- archive-naive-purego-linux-amd64
|
||||
- archive-naive-purego-linux-arm64
|
||||
- archive-naive-purego-windows-amd64
|
||||
- archive-naive-purego-windows-arm64
|
||||
- archive-compressed
|
||||
- archive-openwrt
|
||||
skip_upload: true
|
||||
|
||||
+12
-7
@@ -1,18 +1,23 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
|
||||
LABEL maintainer="shtorm-7"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG GOPROXY=""
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
ENV CGO_ENABLED=0
|
||||
ARG CRONET_GO_PATH=/go/src/github.com/sagernet/sing-box/cronet-go
|
||||
ENV GOPROXY=${GOPROXY}
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOOS=$TARGETOS
|
||||
ENV GOARCH=$TARGETARCH
|
||||
ENV CRONET_GO_PATH=${CRONET_GO_PATH}
|
||||
RUN set -ex \
|
||||
&& apk add git build-base \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \
|
||||
&& git config --global --add safe.directory /go/src/github.com/sagernet/sing-box \
|
||||
&& export GOTOOLCHAIN=local \
|
||||
&& export CGO_ENABLED=1 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
&& eval "$(CGO_ENABLED=0 GOOS= GOARCH= go run -C "$CRONET_GO_PATH" ./cmd/build-naive --target=$GOOS/$GOARCH --libc=musl env --export)" \
|
||||
&& export CGO_LDFLAGS="$CGO_LDFLAGS -Wl,-z,notext -Wl,-z,execstack" \
|
||||
&& export VERSION=$(CGO_ENABLED=0 GOOS= GOARCH= go run ./cmd/internal/read_tag) \
|
||||
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_DOCKER) \
|
||||
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
|
||||
&& go build -v -trimpath -tags "$TAGS" \
|
||||
-o /go/bin/sing-box \
|
||||
|
||||
@@ -21,6 +21,8 @@ SINGBOX_RR_BUILT_BINARY=$(PKG_BUILD_DIR)/sing-box
|
||||
SINGBOX_RR_GOCACHE?=$(TMP_DIR)/sing-box-rr-go-build
|
||||
SINGBOX_RR_GOMODCACHE?=$(DL_DIR)/sing-box-rr-go-mod
|
||||
SINGBOX_RR_GOPROXY?=https://proxy.golang.org|direct
|
||||
SINGBOX_RR_GOTOOLCHAIN?=$(if $(GOTOOLCHAIN),$(GOTOOLCHAIN),$(shell sed -nE 's/^go ([0-9]+([.][0-9]+){1,2}).*$$/go\1+auto/p' $(PROJECT_DIR)/go.mod 2>/dev/null | head -n1))
|
||||
SINGBOX_RR_GOTOOLCHAIN:=$(if $(SINGBOX_RR_GOTOOLCHAIN),$(SINGBOX_RR_GOTOOLCHAIN),auto)
|
||||
SINGBOX_RR_TAGS:=$(shell tr -d '\n' < $(PROJECT_DIR)/release/DEFAULT_BUILD_TAGS_OTHERS 2>/dev/null)
|
||||
SINGBOX_RR_BASE_LDFLAGS:=$(shell tr -d '\n' < $(PROJECT_DIR)/release/LDFLAGS 2>/dev/null)
|
||||
SINGBOX_RR_LDFLAGS:=$(strip $(SINGBOX_RR_BASE_LDFLAGS) -X github.com/sagernet/sing-box/constant.Version=$(SINGBOX_RR_PROJECT_VERSION) -s -w)
|
||||
@@ -124,7 +126,7 @@ define Build/Compile
|
||||
$$$$cc_env \
|
||||
$$$$cxx_env \
|
||||
PATH="$(dir $(GO_BIN)):$$$$PATH" \
|
||||
GOTOOLCHAIN=local \
|
||||
GOTOOLCHAIN="$(SINGBOX_RR_GOTOOLCHAIN)" \
|
||||
GOCACHE="$(SINGBOX_RR_GOCACHE)" \
|
||||
GOMODCACHE="$(SINGBOX_RR_GOMODCACHE)" \
|
||||
GOPROXY="$(SINGBOX_RR_GOPROXY)" \
|
||||
|
||||
@@ -22,26 +22,28 @@ ADMIN_PANEL_TAGS = $(TAGS),with_admin_panel
|
||||
DOCKER_IMAGE ?= shtorm7/sing-box-extended
|
||||
DOCKER_PLATFORMS ?= linux/amd64,linux/arm64
|
||||
|
||||
.PHONY: test release docs build
|
||||
CRONET_GO_PATH ?= $(shell pwd)/cronet-go
|
||||
|
||||
.PHONY: test release docs build schema
|
||||
|
||||
build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
admin_panel_web:
|
||||
build_admin_panel:
|
||||
cd $(ADMIN_PANEL_WEB) && \
|
||||
npm install --no-fund --no-audit && \
|
||||
npm run build
|
||||
|
||||
admin_panel_pack:
|
||||
go run ./cmd/internal/admin_panel_pack \
|
||||
-dir $(ADMIN_PANEL_DIST)
|
||||
|
||||
admin_panel_regen: admin_panel_web admin_panel_pack
|
||||
|
||||
build_admin_panel:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) -tags "$(ADMIN_PANEL_TAGS)" $(MAIN)
|
||||
build_naive:
|
||||
cd $(CRONET_GO_PATH) && \
|
||||
for arch in amd64 arm64 386 arm mipsle mips64le riscv64 loong64; do \
|
||||
go run ./cmd/build-naive --target=linux/$$arch download-toolchain; \
|
||||
done && \
|
||||
for arch in amd64 arm64 386 arm mipsle riscv64 loong64; do \
|
||||
go run ./cmd/build-naive --target=linux/$$arch --libc=musl download-toolchain; \
|
||||
done
|
||||
|
||||
race:
|
||||
export GOTOOLCHAIN=local && \
|
||||
@@ -55,35 +57,31 @@ ci_build:
|
||||
generate_completions:
|
||||
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||
|
||||
schema:
|
||||
go run -ldflags "$(LDFLAGS_SHARED)" --tags "$(TAGS)" $(MAIN) schema -o docs/schema.json
|
||||
|
||||
install:
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
fmt:
|
||||
@gofumpt -l -w .
|
||||
@gofmt -s -w .
|
||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||
@golangci-lint fmt
|
||||
|
||||
fmt_docs:
|
||||
go run ./cmd/internal/format_docs
|
||||
|
||||
fmt_install:
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go install -v github.com/daixiang0/gci@latest
|
||||
|
||||
lint:
|
||||
GOOS=linux golangci-lint run ./...
|
||||
GOOS=android golangci-lint run ./...
|
||||
GOOS=windows golangci-lint run ./...
|
||||
GOOS=darwin golangci-lint run ./...
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
# GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
lint_install:
|
||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
@gofumpt -l -w .
|
||||
@gofumpt -l -w .
|
||||
@golangci-lint fmt
|
||||
|
||||
proto_install:
|
||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
@@ -92,13 +90,16 @@ proto_install:
|
||||
update_certificates:
|
||||
go run ./cmd/internal/update_certificates
|
||||
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
|
||||
release: build_admin_panel build_naive
|
||||
CRONET_GO_PATH=$(CRONET_GO_PATH) go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz \
|
||||
dist/*.zip \
|
||||
dist/*.ipk \
|
||||
dist/*.apk \
|
||||
dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
rm -r dist/release
|
||||
|
||||
release_repo:
|
||||
@@ -119,49 +120,69 @@ release_docker:
|
||||
update_android_version:
|
||||
go run ./cmd/internal/update_android_version
|
||||
|
||||
update_desktop_version:
|
||||
go run ./cmd/internal/update_desktop_version
|
||||
|
||||
build_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease && ./gradlew --stop
|
||||
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
|
||||
VERSION_CODE=$$(grep VERSION_CODE ../sing-box-for-android/version.properties | cut -d= -f2); \
|
||||
VERSION_NAME=$$(grep VERSION_NAME ../sing-box-for-android/version.properties | cut -d= -f2); \
|
||||
printf '{\n "version_code": %s,\n "version_name": "%s"\n}\n' "$$VERSION_CODE" "$$VERSION_NAME" > dist/release_android/SFA-version-metadata.json
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
release_android: lib_android update_android_version build_android
|
||||
release_android: lib_android update_android_version build_android upload_android
|
||||
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||
|
||||
build_desktop_windows:
|
||||
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:win
|
||||
|
||||
build_desktop_linux:
|
||||
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:linux
|
||||
|
||||
upload_desktop:
|
||||
mkdir -p dist/release_desktop
|
||||
cp ../sing-box-for-desktop/release/SFW-*.exe dist/release_desktop 2>/dev/null || true
|
||||
cp ../sing-box-for-desktop/release/SFL-*.deb ../sing-box-for-desktop/release/SFL-*.rpm ../sing-box-for-desktop/release/SFL-*.pkg.tar.zst dist/release_desktop 2>/dev/null || true
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_desktop
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_desktop
|
||||
rm -rf dist/release_desktop
|
||||
|
||||
release_desktop: update_desktop_version build_desktop_windows build_desktop_linux upload_desktop
|
||||
|
||||
# TODO: find why and remove `-destination 'generic/platform=iOS'`
|
||||
# TODO: remove xcode clean when fix control widget fixed
|
||||
build_ios:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFI.xcarchive && \
|
||||
xcodebuild clean -scheme SFI && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild clean -scheme SFI -derivedDataPath build/SFI.dd && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -derivedDataPath build/SFI.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
|
||||
upload_ios_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
export_ios_ipa:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \
|
||||
cp build/SFI/sing-box.ipa dist/SFI.ipa
|
||||
build_ios_deb:
|
||||
$(MAKE) -C ../sing-box-for-apple build_ios_deb
|
||||
|
||||
upload_ios_ipa:
|
||||
cd dist && \
|
||||
cp SFI.ipa "SFI-${VERSION}.ipa" && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa"
|
||||
upload_ios_deb:
|
||||
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64.deb"
|
||||
|
||||
release_ios: build_ios upload_ios_app_store
|
||||
|
||||
release_ios_deb: build_ios_deb upload_ios_deb
|
||||
|
||||
build_macos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFM.xcarchive && \
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -derivedDataPath build/SFM.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
|
||||
upload_macos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
@@ -202,18 +223,35 @@ upload_macos_pkg:
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
|
||||
replace_macos_pkg:
|
||||
mkdir -p dist/SFM
|
||||
cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
||||
cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
|
||||
upload_macos_dsyms:
|
||||
mkdir -p dist/SFM
|
||||
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
|
||||
cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
|
||||
replace_macos_dsyms:
|
||||
mkdir -p dist/SFM
|
||||
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
|
||||
cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
|
||||
release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
|
||||
|
||||
replace_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
|
||||
|
||||
build_tvos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFT.xcarchive && \
|
||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -derivedDataPath build/SFT.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
|
||||
upload_tvos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
@@ -278,8 +316,8 @@ lib_apple_new:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
|
||||
|
||||
lib_install:
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.13
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.13
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
|
||||
+33
-7
@@ -1,12 +1,25 @@
|
||||
# sing-box-rr-edition
|
||||
# sing-box-extended
|
||||
|
||||
Sing-box RR Edition is a network-focused fork with a trimmed feature set, built on top of `sing-box-extended`.
|
||||
[](LICENSE)
|
||||
[](go.mod)
|
||||
[](https://codeberg.org/shtorm-7/sing-box-extended)
|
||||
[](https://t.me/sing_box_extended)
|
||||
|
||||
Sing-box with extended features.
|
||||
|
||||
## 🔥 Features
|
||||
|
||||
### Outbounds
|
||||
### Protocols
|
||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
|
||||
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
|
||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||
- **VPN** — Routed tunnel over any TCP sing-box protocol
|
||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
||||
- **SSH** — SSH client and server with certificate authentication and upstream fallback
|
||||
- **Call** — Traffic tunneling through video-call platforms (VK, Dion, Telemost, WBStream)
|
||||
- **VPN** — Routed tunnel over any sing-box protocol
|
||||
- **Bond** — Link aggregation for increasing throughput
|
||||
- **Fallback** — Outbound group with priority-based switching
|
||||
- **Failover** — Automatic outbound switching with session recovery for high availability
|
||||
|
||||
@@ -14,25 +27,38 @@ Sing-box RR Edition is a network-focused fork with a trimmed feature set, built
|
||||
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
|
||||
- **DNS Fallback** — Sequential / parallel queries across upstream resolvers
|
||||
|
||||
### Limiters
|
||||
- **Bandwidth Limiter** — Upload / download / bidirectional rate limiting
|
||||
- **Connection Limiter** — Concurrent connection control
|
||||
- **Traffic Limiter** — Per-user traffic quotas
|
||||
- **Rate Limiter** — Request rate limiting
|
||||
|
||||
### Encryption & Obfuscation
|
||||
- **Amnezia 3.1** — WireGuard traffic obfuscation
|
||||
- **VLESS encryption** — XRAY encryption for VLESS protocol
|
||||
|
||||
### Transports
|
||||
- **mKCP** — Reliable UDP-based transport
|
||||
- **XHTTP** — Modern XRAY transport
|
||||
- **Rmux** — Improved smux multiplexer
|
||||
|
||||
### Services
|
||||
- **SSM API** — Service API retained in the trimmed runtime
|
||||
- **Resolved** — Linux resolved integration
|
||||
- **Admin Panel** — Web-based management interface
|
||||
- **Manager** — Management service for configuring users, nodes, limiters
|
||||
- **Manager API (HTTP/gRPC)** — HTTP and gRPC API for the Manager
|
||||
- **Node Manager API** — Service for connecting nodes to remote manager
|
||||
|
||||
### Miscellaneous
|
||||
- **Providers** — Outbound subscriptions from local files, inline lists, or remote URLs (sing-box JSON, Clash YAML, SIP008, share links)
|
||||
- **Link Parser** — Outbound configured from a share link (VLESS, VMess, Shadowsocks, Trojan, Hysteria, Hysteria2, TUIC, AnyTLS)
|
||||
- **Extended WireGuard options** — Advanced configuration capabilities
|
||||
- **Unified Delay** — Unified latency measurement
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
Configuration examples are available here:
|
||||
|
||||
./examples
|
||||
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
|
||||
|
||||
## Support the Project
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
ExclusiveAnchors() bool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package certificate
|
||||
|
||||
type Adapter struct {
|
||||
providerType string
|
||||
providerTag string
|
||||
}
|
||||
|
||||
func NewAdapter(providerType string, providerTag string) Adapter {
|
||||
return Adapter{
|
||||
providerType: providerType,
|
||||
providerTag: providerTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.providerType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.providerTag
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.CertificateProviderRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
providers []adapter.CertificateProviderService
|
||||
providerByTag map[string]adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.CertificateProviderService),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err := adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
providers := m.providers
|
||||
m.providers = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace("close ", name)
|
||||
startTime := time.Now()
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, provider.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.providers
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
m.access.Unlock()
|
||||
return provider, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == provider
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return provider.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
|
||||
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err = adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
}
|
||||
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsProvider.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == existsProvider
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.CertificateProviderService, error)
|
||||
|
||||
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(providerType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(providerType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[providerType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[providerType]
|
||||
if !loaded {
|
||||
return nil, E.New("certificate provider type not found: " + providerType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[providerType] = optionsConstructor
|
||||
m.constructor[providerType] = constructor
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package adapter
|
||||
|
||||
import "unsafe"
|
||||
|
||||
type AppleAnchors interface {
|
||||
Retain() AppleAnchors
|
||||
Release()
|
||||
// Ref returns the underlying CFArrayRef, or nil if the anchor set is empty.
|
||||
Ref() unsafe.Pointer
|
||||
}
|
||||
|
||||
type AppleCertificateStore interface {
|
||||
CertificateStore
|
||||
AppleAnchors() AppleAnchors
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type CertificateProvider interface {
|
||||
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
}
|
||||
|
||||
type ACMECertificateProvider interface {
|
||||
CertificateProvider
|
||||
GetACMENextProtos() []string
|
||||
}
|
||||
|
||||
type CertificateProviderService interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
CertificateProvider
|
||||
}
|
||||
|
||||
type CertificateProviderRegistry interface {
|
||||
option.CertificateProviderOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
|
||||
}
|
||||
|
||||
type CertificateProviderManager interface {
|
||||
Lifecycle
|
||||
CertificateProviders() []CertificateProviderService
|
||||
Get(tag string) (CertificateProviderService, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
|
||||
}
|
||||
+42
-21
@@ -3,6 +3,7 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
type DNSRouter interface {
|
||||
Lifecycle
|
||||
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, message *dns.Msg, options DNSQueryOptions, callback func(response *dns.Msg, err error))
|
||||
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
LookupReverseMapping(ip netip.Addr) (string, bool)
|
||||
@@ -25,35 +27,41 @@ type DNSRouter interface {
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool, callback func(response *dns.Msg, err error))
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
DisableOptimisticCache bool
|
||||
RewriteTTL *uint32
|
||||
Timeout time.Duration
|
||||
ClientSubnet netip.Prefix
|
||||
RemoveClientSubnet bool
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
if options == nil {
|
||||
return &DNSQueryOptions{}, nil
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) {
|
||||
if options == nil || options.Server == "" {
|
||||
return DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded := transportManager.Transport(options.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + options.Server)
|
||||
return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
return DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
Timeout: time.Duration(options.Timeout),
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -63,6 +71,13 @@ type RDRCStore interface {
|
||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||
}
|
||||
|
||||
type DNSCacheStore interface {
|
||||
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
||||
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
||||
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
||||
ClearDNSCache() error
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
@@ -72,11 +87,17 @@ type DNSTransport interface {
|
||||
// Exchanges that are currently using those connections may fail.
|
||||
Reset()
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, message *dns.Msg, callback func(response *dns.Msg, err error))
|
||||
}
|
||||
|
||||
type LegacyDNSTransport interface {
|
||||
LegacyStrategy() C.DomainStrategy
|
||||
LegacyClientSubnet() netip.Prefix
|
||||
type DNSTransportWithPreferredDomain interface {
|
||||
DNSTransport
|
||||
PreferredDomain(domain string) bool
|
||||
}
|
||||
|
||||
type DNSTransportWithEnvironment interface {
|
||||
DNSTransport
|
||||
Environment() []string
|
||||
}
|
||||
|
||||
type DNSTransportRegistry interface {
|
||||
|
||||
@@ -2,6 +2,8 @@ package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
@@ -7,17 +7,17 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
LifecycleService
|
||||
ConnectionTracker
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
SetModeUpdateHook(hook *observable.Subscriber[struct{}])
|
||||
HistoryStorage() URLTestHistoryStorage
|
||||
SetMode(mode string)
|
||||
AddModeUpdateHook(hook *observable.Subscriber[struct{}])
|
||||
}
|
||||
|
||||
type URLTestHistory struct {
|
||||
@@ -25,14 +25,6 @@ type URLTestHistory struct {
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
type URLTestHistoryStorage interface {
|
||||
SetHook(hook *observable.Subscriber[struct{}])
|
||||
LoadURLTestHistory(tag string) *URLTestHistory
|
||||
DeleteURLTestHistory(tag string)
|
||||
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
LifecycleService
|
||||
StatsService() ConnectionTracker
|
||||
@@ -41,6 +33,8 @@ type V2RayServer interface {
|
||||
type CacheFile interface {
|
||||
LifecycleService
|
||||
|
||||
CacheID() string
|
||||
|
||||
StoreFakeIP() bool
|
||||
FakeIPStorage
|
||||
|
||||
@@ -49,6 +43,13 @@ type CacheFile interface {
|
||||
|
||||
StoreWARPConfig() bool
|
||||
StoreMASQUEConfig() bool
|
||||
StoreProviders() bool
|
||||
|
||||
StoreDNS() bool
|
||||
DNSCacheStore
|
||||
|
||||
SetDisableExpire(disableExpire bool)
|
||||
SetOptimisticTimeout(timeout time.Duration)
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
@@ -70,11 +71,12 @@ type SavedBinary struct {
|
||||
Content []byte
|
||||
LastUpdated time.Time
|
||||
LastEtag string
|
||||
URLHash []byte
|
||||
}
|
||||
|
||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(2))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,6 +100,14 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.URLHash)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buffer.Write(s.URLHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
@@ -112,6 +122,9 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contentLength > uint64(reader.Len()) {
|
||||
return E.New("invalid content length: ", contentLength)
|
||||
}
|
||||
s.Content = make([]byte, contentLength)
|
||||
_, err = io.ReadFull(reader, s.Content)
|
||||
if err != nil {
|
||||
@@ -127,12 +140,30 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if etagLength > uint64(reader.Len()) {
|
||||
return E.New("invalid etag length: ", etagLength)
|
||||
}
|
||||
etagBytes := make([]byte, etagLength)
|
||||
_, err = io.ReadFull(reader, etagBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastEtag = string(etagBytes)
|
||||
if version < 2 {
|
||||
return nil
|
||||
}
|
||||
urlHashLength, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if urlHashLength > uint64(reader.Len()) {
|
||||
return E.New("invalid url hash length: ", urlHashLength)
|
||||
}
|
||||
s.URLHash = make([]byte, urlHashLength)
|
||||
_, err = io.ReadFull(reader, s.URLHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -145,11 +176,5 @@ type OutboundGroup interface {
|
||||
type URLTestGroup interface {
|
||||
OutboundGroup
|
||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
||||
}
|
||||
|
||||
func OutboundTag(detour Outbound) string {
|
||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||
return group.Now()
|
||||
}
|
||||
return detour.Tag()
|
||||
PerformUpdateCheck()
|
||||
}
|
||||
|
||||
@@ -5,57 +5,31 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
type ConnectionHandler interface {
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
type ConnectionHandlerEx interface {
|
||||
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use PacketHandlerEx instead
|
||||
type PacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
||||
NewPacket(buffer *buf.Buffer, source M.Socksaddr)
|
||||
}
|
||||
|
||||
type PacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
|
||||
type PacketBatchHandler interface {
|
||||
NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated: use OOBPacketHandlerEx instead
|
||||
type OOBPacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
||||
NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
}
|
||||
|
||||
type OOBPacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
type PacketConnectionHandler interface {
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
type PacketConnectionHandlerEx interface {
|
||||
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use TCPConnectionHandlerEx instead
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type UpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
type UpstreamHandlerAdapterEx interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
N.UDPConnectionHandlerEx
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type HTTPTransport interface {
|
||||
http.RoundTripper
|
||||
CloseIdleConnections()
|
||||
Reset()
|
||||
}
|
||||
|
||||
type HTTPClientManager interface {
|
||||
ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (HTTPTransport, error)
|
||||
DefaultTransport() HTTPTransport
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type HTTPStartContext struct {
|
||||
access sync.Mutex
|
||||
transports []HTTPTransport
|
||||
}
|
||||
|
||||
func NewHTTPStartContext() *HTTPStartContext {
|
||||
return &HTTPStartContext{}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Register(transport HTTPTransport) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.transports = append(c.transports, transport)
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
for _, transport := range c.transports {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,17 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tlsspoof"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
@@ -19,12 +23,12 @@ type Inbound interface {
|
||||
|
||||
type TCPInjectableInbound interface {
|
||||
Inbound
|
||||
ConnectionHandlerEx
|
||||
ConnectionHandler
|
||||
}
|
||||
|
||||
type UDPInjectableInbound interface {
|
||||
Inbound
|
||||
PacketConnectionHandlerEx
|
||||
PacketConnectionHandler
|
||||
}
|
||||
|
||||
type InboundRegistry interface {
|
||||
@@ -52,6 +56,11 @@ type InboundContext struct {
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// power report
|
||||
|
||||
RouteRule string
|
||||
RouteOutbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
@@ -74,18 +83,28 @@ type InboundContext struct {
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
TLSRecordFragment bool
|
||||
TLSSpoof string
|
||||
TLSSpoofMethod tlsspoof.Method
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
DestinationAddresses []netip.Addr
|
||||
DNSResponse *dns.Msg
|
||||
NamedDNSResponses map[string]*dns.Msg
|
||||
DestinationAddressMatchFromResponse bool
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
QueryClientSubnet netip.Prefix
|
||||
QueryDNSSEC bool
|
||||
FakeIP bool
|
||||
PreMatch bool
|
||||
|
||||
// rule cache
|
||||
|
||||
@@ -96,7 +115,7 @@ type InboundContext struct {
|
||||
SourcePortMatch bool
|
||||
DestinationAddressMatch bool
|
||||
DestinationPortMatch bool
|
||||
DidMatch bool
|
||||
DeferredIPCIDRMatchGroups uint8
|
||||
IgnoreDestinationIPCIDRMatch bool
|
||||
}
|
||||
|
||||
@@ -111,11 +130,67 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.SourcePortMatch = false
|
||||
c.DestinationAddressMatch = false
|
||||
c.DestinationPortMatch = false
|
||||
c.DidMatch = false
|
||||
c.DeferredIPCIDRMatchGroups = 0
|
||||
}
|
||||
|
||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
||||
return DNSResponseAddresses(c.DNSResponse)
|
||||
}
|
||||
|
||||
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||
return nil
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawRecord := range response.Answer {
|
||||
switch record := rawRecord.(type) {
|
||||
case *dns.A:
|
||||
addr := M.AddrFromIP(record.A)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.AAAA:
|
||||
addr := M.AddrFromIP(record.AAAA)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.HTTPS:
|
||||
for _, value := range record.SVCB.Value {
|
||||
switch hint := value.(type) {
|
||||
case *dns.SVCBIPv4Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip).Unmap()
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
case *dns.SVCBIPv6Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type inboundContextKey struct{}
|
||||
|
||||
type dnsTransportTagKey struct{}
|
||||
|
||||
func ContextWithDNSTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, (*dnsTransportTagKey)(nil), transportTag)
|
||||
}
|
||||
|
||||
func DNSTransportTagFromContext(ctx context.Context) (string, bool) {
|
||||
transportTag, loaded := ctx.Value((*dnsTransportTagKey)(nil)).(string)
|
||||
return transportTag, loaded
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||
return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipv4Hint := net.ParseIP("1.1.1.1")
|
||||
require.NotNil(t, ipv4Hint)
|
||||
|
||||
response := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.HTTPS{
|
||||
SVCB: dns.SVCB{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: dns.Fqdn("example.com"),
|
||||
Rrtype: dns.TypeHTTPS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
Priority: 1,
|
||||
Target: ".",
|
||||
Value: []dns.SVCBKeyValue{
|
||||
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
addresses := DNSResponseAddresses(response)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
|
||||
require.True(t, addresses[0].Is4())
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -74,11 +75,15 @@ func getServiceName(service any) string {
|
||||
return strings.ToLower(t.Name())
|
||||
}
|
||||
|
||||
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
|
||||
func Start(ctx context.Context, logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := getServiceName(service)
|
||||
done := LogElapsed(logger, stage, " ", name)
|
||||
err := service.Start(stage)
|
||||
err = service.Start(stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -87,10 +92,14 @@ func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
|
||||
func StartNamed(ctx context.Context, logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
|
||||
for _, service := range services {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := LogElapsed(logger, stage, " ", service.Name())
|
||||
err := service.Start(stage)
|
||||
err = service.Start(stage)
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage.String(), " ", service.Name())
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type NeighborEntry struct {
|
||||
Address netip.Addr
|
||||
MACAddress net.HardwareAddr
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type NeighborResolver interface {
|
||||
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
|
||||
LookupHostname(address netip.Addr) (string, bool)
|
||||
LookupAddresses(hostname string) []netip.Addr
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type NeighborUpdateListener interface {
|
||||
UpdateNeighborTable(entries []NeighborEntry)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package adapter
|
||||
|
||||
type NetworkNamespaceManager interface {
|
||||
ResolvePath(nameOrPath string) string
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +20,7 @@ type NetworkManager interface {
|
||||
UpdateInterfaces() error
|
||||
DefaultNetworkInterface() *NetworkInterface
|
||||
NetworkInterfaces() []NetworkInterface
|
||||
NetworkEnvironment() uint64
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
ProtectFunc() control.Func
|
||||
@@ -30,8 +33,8 @@ type NetworkManager interface {
|
||||
PackageManager() tun.PackageManager
|
||||
NeedWIFIState() bool
|
||||
WIFIState() WIFIState
|
||||
UpdateWIFIState()
|
||||
ResetNetwork()
|
||||
UpdateWIFIState(ctx context.Context)
|
||||
ResetNetwork(ctx context.Context)
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
@@ -46,7 +49,7 @@ type NetworkOptions struct {
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
InterfaceUpdated(ctx context.Context)
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
@@ -76,6 +79,7 @@ type NetworkInterface struct {
|
||||
control.Interface
|
||||
Type C.InterfaceType
|
||||
DNSServers []string
|
||||
Gateways []netip.Addr
|
||||
Expensive bool
|
||||
Constrained bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenConnectStateConnecting = "connecting"
|
||||
OpenConnectStateAuthPending = "auth-pending"
|
||||
OpenConnectStateConnected = "connected"
|
||||
OpenConnectStateError = "error"
|
||||
)
|
||||
|
||||
type OpenConnectEndpoint interface {
|
||||
Endpoint
|
||||
OpenConnectStatus() OpenConnectStatus
|
||||
StatusUpdated() <-chan struct{}
|
||||
CompleteAuthChallenge(challengeID string, response OpenConnectAuthResponse) error
|
||||
CancelAuthChallenge(challengeID string) error
|
||||
}
|
||||
|
||||
type OpenConnectStatus struct {
|
||||
State string
|
||||
AuthChallenge *OpenConnectAuthChallenge
|
||||
Error string
|
||||
TunnelInfo *OpenConnectTunnelInfo
|
||||
}
|
||||
|
||||
type OpenConnectTunnelInfo struct {
|
||||
Server string
|
||||
Flavor string
|
||||
Transport string
|
||||
IPv4 []netip.Prefix
|
||||
IPv6 []netip.Prefix
|
||||
DNS []netip.Addr
|
||||
MTU uint32
|
||||
ConnectedSince time.Time
|
||||
}
|
||||
|
||||
type OpenConnectAuthChallenge struct {
|
||||
ID string
|
||||
Banner string
|
||||
Message string
|
||||
Error string
|
||||
Form *OpenConnectAuthForm
|
||||
Browser *OpenConnectBrowserRequest
|
||||
}
|
||||
|
||||
type OpenConnectAuthForm struct {
|
||||
Fields []OpenConnectAuthFormField
|
||||
}
|
||||
|
||||
type OpenConnectBrowserRequest struct {
|
||||
URL string
|
||||
FinalURL string
|
||||
CookieNames []string
|
||||
EarlyCookieNames []string
|
||||
HeaderNames []string
|
||||
CallbackURLPrefixes []string
|
||||
CacheID string
|
||||
}
|
||||
|
||||
type OpenConnectBrowserCookie struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type OpenConnectBrowserHeader struct {
|
||||
Name string
|
||||
Values []string
|
||||
}
|
||||
|
||||
type OpenConnectAuthResponse struct {
|
||||
Form *OpenConnectAuthFormResponse
|
||||
Browser *OpenConnectBrowserResult
|
||||
}
|
||||
|
||||
type OpenConnectAuthFormResponse struct {
|
||||
Values map[string]string
|
||||
}
|
||||
|
||||
type OpenConnectBrowserResult struct {
|
||||
FinalURL string
|
||||
Cookies []OpenConnectBrowserCookie
|
||||
Headers []OpenConnectBrowserHeader
|
||||
}
|
||||
|
||||
type OpenConnectAuthFormField struct {
|
||||
SubmissionKey string
|
||||
Name string
|
||||
Label string
|
||||
Kind string
|
||||
Value string
|
||||
Options []OpenConnectAuthFormChoice
|
||||
}
|
||||
|
||||
type OpenConnectAuthFormChoice struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenVPNStateConnecting = "connecting"
|
||||
OpenVPNStateAuthPending = "auth-pending"
|
||||
OpenVPNStateConnected = "connected"
|
||||
OpenVPNStateError = "error"
|
||||
)
|
||||
|
||||
type OpenVPNEndpoint interface {
|
||||
Endpoint
|
||||
OpenVPNStatus() OpenVPNStatus
|
||||
StatusUpdated() <-chan struct{}
|
||||
CompleteChallenge(challengeID string, response OpenVPNChallengeResponse) error
|
||||
CancelChallenge(challengeID string) error
|
||||
}
|
||||
|
||||
type OpenVPNStatus struct {
|
||||
State string
|
||||
Challenge *OpenVPNChallenge
|
||||
Error string
|
||||
TunnelInfo *OpenVPNTunnelInfo
|
||||
}
|
||||
|
||||
type OpenVPNTunnelInfo struct {
|
||||
Server string
|
||||
Network string
|
||||
Cipher string
|
||||
IPv4 []netip.Prefix
|
||||
IPv6 []netip.Prefix
|
||||
DNS []netip.Addr
|
||||
MTU uint32
|
||||
ConnectedSince time.Time
|
||||
}
|
||||
|
||||
type OpenVPNChallenge struct {
|
||||
ID string
|
||||
Kind string
|
||||
Username string
|
||||
Message string
|
||||
URL string
|
||||
SecretMessage string
|
||||
Echo bool
|
||||
PreviousError string
|
||||
Deadline time.Time
|
||||
}
|
||||
|
||||
type OpenVPNChallengeResponse struct {
|
||||
Username string
|
||||
Password string
|
||||
Secret string
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -23,8 +22,8 @@ type Outbound interface {
|
||||
|
||||
type OutboundWithPreferredRoutes interface {
|
||||
Outbound
|
||||
PreferredDomain(domain string) bool
|
||||
PreferredAddress(address netip.Addr) bool
|
||||
PreferredDomain(metadata *InboundContext, domain string) bool
|
||||
PreferredAddress(metadata *InboundContext, address netip.Addr) bool
|
||||
}
|
||||
|
||||
type OutboundWithMultiplex interface {
|
||||
@@ -32,9 +31,10 @@ type OutboundWithMultiplex interface {
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
type DirectRouteOutbound interface {
|
||||
type FlowOutbound interface {
|
||||
Outbound
|
||||
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
|
||||
tun.Port
|
||||
PreMatchFlow(network string, destination netip.Addr) PreMatchAction
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
|
||||
@@ -233,7 +233,7 @@ func (m *Manager) Remove(tag string) error {
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Info("updated default outbound to ", outbound.Tag())
|
||||
m.logger.Notice("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -2,6 +2,8 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) OptionTypes() []string {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(r.optionsType))
|
||||
}
|
||||
|
||||
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -13,9 +14,11 @@ type PlatformInterface interface {
|
||||
|
||||
UsePlatformAutoDetectInterfaceControl() bool
|
||||
AutoDetectInterfaceControl(fd int) error
|
||||
BindInterfaceControl(fd int, interfaceName string) error
|
||||
|
||||
UsePlatformInterface() bool
|
||||
OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
|
||||
ProcessPlatformOptions(options option.TunPlatformOptions) error
|
||||
|
||||
UsePlatformDefaultInterfaceMonitor() bool
|
||||
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
|
||||
@@ -28,8 +31,7 @@ type PlatformInterface interface {
|
||||
|
||||
ClearDNSCache()
|
||||
RequestPermissionForWIFIState() error
|
||||
ReadWIFIState() WIFIState
|
||||
SystemCertificates() []string
|
||||
ReadWIFIState(ctx context.Context) WIFIState
|
||||
|
||||
UsePlatformConnectionOwnerFinder() bool
|
||||
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
|
||||
@@ -38,8 +40,51 @@ type PlatformInterface interface {
|
||||
|
||||
UsePlatformNotification() bool
|
||||
SendNotification(notification *Notification) error
|
||||
CancelNotification(identifier string, typeID int32) error
|
||||
|
||||
MyInterfaceAddress() []netip.Addr
|
||||
|
||||
UsePlatformNeighborResolver() bool
|
||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||
|
||||
UsePlatformShell() bool
|
||||
CheckPlatformShell() error
|
||||
OpenShellSession(user *PlatformUser, command string, env []string, term string, rows int32, cols int32) (ShellSession, error)
|
||||
LookupUser(username string) (*PlatformUser, error)
|
||||
LookupSFTPServer() (string, error)
|
||||
ReadSystemSSHHostKey() ([]byte, error)
|
||||
TailscaleHostname() string
|
||||
|
||||
UsePlatformBridge() bool
|
||||
CreateBridge(options BridgeOptions) (BridgeSession, error)
|
||||
}
|
||||
|
||||
type BridgeOptions struct {
|
||||
BridgeName string
|
||||
MTU uint32
|
||||
Inet4Port netip.Addr
|
||||
Inet6Port netip.Addr
|
||||
Interface string
|
||||
RuleIndex int
|
||||
RouteTable int
|
||||
}
|
||||
|
||||
type BridgeSession interface {
|
||||
FileDescriptor() int
|
||||
Name() string
|
||||
Inet6Active() bool
|
||||
SetEgress(interfaceName string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type PlatformUser struct {
|
||||
Username string
|
||||
Uid int
|
||||
Gid int
|
||||
HomeDir string
|
||||
Shell string
|
||||
Groups []int
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
|
||||
@@ -3,6 +3,8 @@ package provider
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -30,14 +32,15 @@ type Adapter struct {
|
||||
outboundsByTag map[string]adapter.Outbound
|
||||
ticker *time.Ticker
|
||||
checking atomic.Bool
|
||||
history adapter.URLTestHistoryStorage
|
||||
history *urltest.HistoryStorage
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||
|
||||
link string
|
||||
enabled bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||
@@ -68,14 +71,14 @@ func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.Out
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) SetRemoveEmojis(remove bool) {
|
||||
a.removeEmojis = remove
|
||||
}
|
||||
|
||||
func (a *Adapter) Start() error {
|
||||
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
|
||||
a.history = service.PtrFromContext[urltest.HistoryStorage](a.ctx)
|
||||
if a.history == nil {
|
||||
if clashServer := service.FromContext[adapter.ClashServer](a.ctx); clashServer != nil {
|
||||
a.history = clashServer.HistoryStorage()
|
||||
} else {
|
||||
a.history = urltest.NewHistoryStorage()
|
||||
}
|
||||
a.history = urltest.NewHistoryStorage()
|
||||
}
|
||||
go a.loopCheck()
|
||||
return nil
|
||||
@@ -102,6 +105,10 @@ func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
}
|
||||
|
||||
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
|
||||
if a.removeEmojis {
|
||||
removeEmojisFromTags(newOpts)
|
||||
}
|
||||
uniquifyTags(newOpts)
|
||||
a.removeUseless(newOpts)
|
||||
var (
|
||||
oldOptByTag = make(map[string]option.Outbound)
|
||||
@@ -265,3 +272,34 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uniquifyTags(opts []option.Outbound) {
|
||||
count := make(map[string]int)
|
||||
for i, opt := range opts {
|
||||
count[opt.Tag]++
|
||||
if count[opt.Tag] > 1 {
|
||||
opts[i].Tag = F.ToString(opt.Tag, " #", count[opt.Tag])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEmojisFromTags(opts []option.Outbound) {
|
||||
for i, opt := range opts {
|
||||
cleaned := flagRegex.ReplaceAllStringFunc(opt.Tag, flagToCountryCode)
|
||||
cleaned = emojiRegex.ReplaceAllString(cleaned, "")
|
||||
cleaned = multiSpaceRegex.ReplaceAllString(cleaned, " ")
|
||||
opts[i].Tag = strings.TrimSpace(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
func flagToCountryCode(flag string) string {
|
||||
runes := []rune(flag)
|
||||
if len(runes) == 2 {
|
||||
return string(rune(runes[0]-0x1F1E6+'A')) + string(rune(runes[1]-0x1F1E6+'A')) + " "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var flagRegex = regexp.MustCompile(`[\x{1F1E6}-\x{1F1FF}]{2}`)
|
||||
var emojiRegex = regexp.MustCompile(`[\x{1F1E0}-\x{1F1FF}\x{1F300}-\x{1F9FF}\x{2600}-\x{27BF}\x{FE00}-\x{FE0F}\x{200D}]+`)
|
||||
var multiSpaceRegex = regexp.MustCompile(`\s{2,}`)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func TestFlagToCountryCodeAllFlags(t *testing.T) {
|
||||
for first := 'A'; first <= 'Z'; first++ {
|
||||
for second := 'A'; second <= 'Z'; second++ {
|
||||
flag := string(rune(0x1F1E6+(first-'A'))) + string(rune(0x1F1E6+(second-'A')))
|
||||
expected := string(first) + string(second)
|
||||
result := flagToCountryCode(flag)
|
||||
// flagToCountryCode appends a space
|
||||
if result != expected+" " {
|
||||
t.Errorf("flagToCountryCode(%q) = %q, want %q", expected, result, expected+" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEmojisFromTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"🇺🇸 United States", "US United States"},
|
||||
{"🇷🇺 Россия", "RU Россия"},
|
||||
{"🇩🇪 Germany 🚀", "DE Germany"},
|
||||
{"🇫🇷🇬🇧 France-UK", "FR GB France-UK"},
|
||||
{"No emojis here", "No emojis here"},
|
||||
{"🌍 World", "World"},
|
||||
{"🇯🇵 Tokyo ⚡ Fast", "JP Tokyo Fast"},
|
||||
{"Germany 🇩🇪", "Germany DE"},
|
||||
{"Server 🇺🇸 Node", "Server US Node"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
opts := []option.Outbound{{Tag: tt.input}}
|
||||
removeEmojisFromTags(opts)
|
||||
if opts[0].Tag != tt.expected {
|
||||
t.Errorf("removeEmojisFromTags(%q) = %q, want %q", tt.input, opts[0].Tag, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,14 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing-tun/gtcpip/header"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -21,18 +18,93 @@ import (
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error)
|
||||
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
|
||||
HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type PreMatchAction uint8
|
||||
|
||||
const (
|
||||
PreMatchContinue PreMatchAction = iota
|
||||
PreMatchFlow
|
||||
PreMatchReject
|
||||
PreMatchDrop
|
||||
PreMatchBypass
|
||||
PreMatchHijackDNS
|
||||
)
|
||||
|
||||
type PreMatchResult struct {
|
||||
Action PreMatchAction
|
||||
Outbound Outbound
|
||||
Destination netip.AddrPort
|
||||
UDPTimeout time.Duration
|
||||
NewTracker func() tun.FlowTracker
|
||||
}
|
||||
|
||||
func JudgeFlow(router Router, inbound string, inboundType string, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
|
||||
var networkName string
|
||||
switch network {
|
||||
case uint8(header.TCPProtocolNumber):
|
||||
networkName = N.NetworkTCP
|
||||
case uint8(header.UDPProtocolNumber):
|
||||
networkName = N.NetworkUDP
|
||||
case uint8(header.ICMPv4ProtocolNumber), uint8(header.ICMPv6ProtocolNumber):
|
||||
networkName = N.NetworkICMP
|
||||
default:
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
metadata := InboundContext{
|
||||
Inbound: inbound,
|
||||
InboundType: inboundType,
|
||||
Network: networkName,
|
||||
Source: M.SocksaddrFromNetIP(source),
|
||||
Destination: M.SocksaddrFromNetIP(destination),
|
||||
}
|
||||
if networkName == N.NetworkICMP {
|
||||
metadata.Source.Port = 0
|
||||
metadata.Destination.Port = 0
|
||||
}
|
||||
result := router.PreMatch(metadata, firstPacket)
|
||||
switch result.Action {
|
||||
case PreMatchFlow:
|
||||
port, isPort := result.Outbound.(tun.Port)
|
||||
if !isPort {
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
|
||||
if result.Destination.IsValid() {
|
||||
destinationPort := result.Destination.Port()
|
||||
if networkName == N.NetworkICMP {
|
||||
destinationPort = destination.Port()
|
||||
}
|
||||
verdict.Destination = netip.AddrPortFrom(result.Destination.Addr(), destinationPort)
|
||||
}
|
||||
return verdict
|
||||
case PreMatchReject:
|
||||
return tun.FlowVerdict{Action: tun.ActionReject}
|
||||
case PreMatchDrop:
|
||||
return tun.FlowVerdict{Action: tun.ActionDrop}
|
||||
case PreMatchBypass:
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass}
|
||||
case PreMatchHijackDNS:
|
||||
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
|
||||
default:
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectionTracker interface {
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn
|
||||
RoutedFlow(ctx context.Context, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) tun.FlowTracker
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
@@ -50,7 +122,6 @@ type ConnectionRouterEx interface {
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
IncRef()
|
||||
@@ -64,51 +135,20 @@ type RuleSet interface {
|
||||
|
||||
type RuleSetUpdateCallback func(it RuleSet)
|
||||
|
||||
type DNSRuleSetUpdateValidator interface {
|
||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
||||
}
|
||||
|
||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
||||
type RuleSetMetadata struct {
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
// ContainsNonIPCIDRRule signals that the rule-set carries at least one sub-rule
|
||||
// with a predicate other than destination ip_cidr / ip_set, so it can contribute
|
||||
// to DNS pre-response matching. A rule-set where this is false and
|
||||
// ContainsIPCIDRRule is true is "pure-IP" and matches nothing before a DNS
|
||||
// response is available.
|
||||
ContainsNonIPCIDRRule bool
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
@@ -18,8 +20,13 @@ type Rule interface {
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
LegacyPreMatch(metadata *InboundContext) bool
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
||||
MatchResponseTag() string
|
||||
MatchResponseTags() []string
|
||||
MatchResponseAnonymous() bool
|
||||
Race() bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
@@ -29,7 +36,7 @@ type RuleAction interface {
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type SecurityPolicy interface {
|
||||
CheckFeature(ctx context.Context, feature string) error
|
||||
}
|
||||
|
||||
func CheckSecurityFeature(ctx context.Context, feature string) error {
|
||||
policy := service.FromContext[SecurityPolicy](ctx)
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
return policy.CheckFeature(ctx, feature)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ func NewRegistry() *Registry {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TailscaleEndpoint interface {
|
||||
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
|
||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
||||
SetTailscaleExitNode(ctx context.Context, stableID string) error
|
||||
Logout(ctx context.Context) error
|
||||
GetTailscaleCertificate(ctx context.Context, domain string, minValidity time.Duration) (certificatePEM []byte, privateKeyPEM []byte, err error)
|
||||
SubscribeTaildropInbox(ctx context.Context, fn func(*TaildropInbox)) error
|
||||
MarkTaildropInboxRead() error
|
||||
SendTaildropFile(ctx context.Context, peerStableID string, fileName string, size int64, content io.Reader, progress func(sentBytes int64)) error
|
||||
OpenTaildropFile(fileName string) (io.ReadCloser, int64, error)
|
||||
DeleteTaildropFile(fileName string) error
|
||||
CancelTaildropReceiving(senderID string, fileName string) error
|
||||
}
|
||||
|
||||
type TaildropInbox struct {
|
||||
Files []*TaildropFile
|
||||
Receiving []*TaildropReceivingFile
|
||||
}
|
||||
|
||||
type TaildropFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
SenderName string
|
||||
ModifiedAt int64
|
||||
}
|
||||
|
||||
type TaildropReceivingFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
ReceivedBytes int64
|
||||
SenderID string
|
||||
SenderName string
|
||||
}
|
||||
|
||||
type TailscalePingResult struct {
|
||||
LatencyMs float64
|
||||
IsDirect bool
|
||||
Endpoint string
|
||||
PeerRelay string
|
||||
DERPRegionID int32
|
||||
DERPRegionCode string
|
||||
Error string
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatus struct {
|
||||
BackendState string
|
||||
AuthURL string
|
||||
NetworkName string
|
||||
MagicDNSSuffix string
|
||||
Self *TailscalePeer
|
||||
ExitNode *TailscalePeer
|
||||
UserGroups []*TailscaleUserGroup
|
||||
KeyAuth bool
|
||||
CanShareFiles bool
|
||||
WaitingFileCount int32
|
||||
ReceivingFileCount int32
|
||||
UnreadFileCount int32
|
||||
CertDomains []string
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
UserID int64
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Peers []*TailscalePeer
|
||||
}
|
||||
|
||||
type TailscalePeer struct {
|
||||
StableID string
|
||||
HostName string
|
||||
DNSName string
|
||||
OS string
|
||||
TailscaleIPs []string
|
||||
SSHHostKeys []string
|
||||
Online bool
|
||||
ExitNode bool
|
||||
ExitNodeOption bool
|
||||
ShareeNode bool
|
||||
Expired bool
|
||||
Active bool
|
||||
CanReceiveFiles bool
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
UserID int64
|
||||
KeyExpiry int64
|
||||
LastSeen int64
|
||||
}
|
||||
|
||||
type ShellSession interface {
|
||||
MasterFD() int32
|
||||
Resize(rows int32, cols int32) error
|
||||
Signal(signal int32) error
|
||||
WaitExit() (int32, error)
|
||||
Close() error
|
||||
}
|
||||
@@ -9,31 +9,31 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
)
|
||||
|
||||
func NewUpstreamHandlerEx(
|
||||
func NewUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamHandlerWrapperEx{
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
|
||||
type myUpstreamHandlerWrapperEx struct {
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := w.metadata
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
@@ -44,7 +44,7 @@ func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn n
|
||||
w.connectionHandler(ctx, conn, myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := w.metadata
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
@@ -55,24 +55,24 @@ func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context,
|
||||
w.packetHandler(ctx, conn, myMetadata, onClose)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil)
|
||||
|
||||
type myUpstreamContextHandlerWrapperEx struct {
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
}
|
||||
|
||||
func NewUpstreamContextHandlerEx(
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamContextHandlerWrapperEx{
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
@@ -83,7 +83,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
||||
w.connectionHandler(ctx, conn, *myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
@@ -94,24 +94,24 @@ func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Co
|
||||
w.packetHandler(ctx, conn, *myMetadata, onClose)
|
||||
}
|
||||
|
||||
func NewRouteHandlerEx(
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeHandlerWrapperEx{
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
type routeHandlerWrapperEx struct {
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Co
|
||||
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
@@ -131,21 +131,21 @@ func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func NewRouteContextHandlerEx(
|
||||
func NewRouteContextHandler(
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeContextHandlerWrapperEx{
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
type routeContextHandlerWrapperEx struct {
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
@@ -156,7 +156,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
||||
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
|
||||
@@ -12,21 +12,30 @@ import (
|
||||
|
||||
type (
|
||||
// Deprecated
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
// Deprecated
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewUpstreamHandler(
|
||||
type LegacyUpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewLegacyUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
connectionHandler LegacyConnectionHandlerFunc,
|
||||
packetHandler LegacyPacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
@@ -34,20 +43,20 @@ func NewUpstreamHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
type legacyUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
connectionHandler LegacyConnectionHandlerFunc
|
||||
packetHandler LegacyPacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -58,8 +67,8 @@ func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.C
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -70,8 +79,8 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
@@ -83,28 +92,28 @@ func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
type legacyUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler LegacyConnectionHandlerFunc
|
||||
packetHandler LegacyPacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func NewLegacyUpstreamContextHandler(
|
||||
connectionHandler LegacyConnectionHandlerFunc,
|
||||
packetHandler LegacyPacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -115,8 +124,8 @@ func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, con
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -127,18 +136,18 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Contex
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteHandler(
|
||||
func NewLegacyRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyRouteHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
@@ -146,29 +155,29 @@ func NewRouteHandler(
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteContextHandler(
|
||||
func NewLegacyRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyRouteContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type routeHandlerWrapper struct {
|
||||
type legacyRouteHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -180,7 +189,7 @@ func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn,
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -192,20 +201,20 @@ func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.Pa
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
func (w *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type routeContextHandlerWrapper struct {
|
||||
type legacyRouteContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -217,7 +226,7 @@ func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -229,6 +238,6 @@ func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, co
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
func (w *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-usbip"
|
||||
)
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
AddDevice(info usbip.ProvidedDeviceInfo, transport usbip.DeviceTransport) (string, error)
|
||||
RemoveDevice(busID string)
|
||||
SubscribeDevices(ctx context.Context, listener func([]usbip.ControlDeviceInfo))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
usbipNotIncluded()
|
||||
}
|
||||
+197
-60
@@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -16,13 +18,17 @@ import (
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/httpclient"
|
||||
"github.com/sagernet/sing-box/common/netns"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
@@ -38,27 +44,33 @@ import (
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
provider *provider.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
ctx context.Context
|
||||
createdAt time.Time
|
||||
debugOptions option.DebugOptions
|
||||
debugHTTPServer *http.Server
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
provider *provider.Manager
|
||||
service *boxService.Manager
|
||||
certificateProvider *boxCertificate.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
NetworkNamespaceHolderArgs []string
|
||||
}
|
||||
|
||||
func Context(
|
||||
@@ -69,6 +81,7 @@ func Context(
|
||||
providerRegistry adapter.ProviderRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
@@ -98,6 +111,10 @@ func Context(
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
|
||||
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -115,6 +132,7 @@ func New(options Options) (*Box, error) {
|
||||
providerRegistry := service.FromContext[adapter.ProviderRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
@@ -134,10 +152,14 @@ func New(options Options) (*Box, error) {
|
||||
if serviceRegistry == nil {
|
||||
return nil, E.New("missing service registry in context")
|
||||
}
|
||||
if certificateProviderRegistry == nil {
|
||||
return nil, E.New("missing certificate provider registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
debugOptions := common.PtrValueOrDefault(experimentalOptions.Debug)
|
||||
err := checkDebugOptions(debugOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -156,6 +178,12 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
|
||||
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
|
||||
}
|
||||
needAPIService := common.Any(options.Services, func(it option.Service) bool {
|
||||
return it.Type == C.TypeAPI
|
||||
})
|
||||
if service.PtrFromContext[urltest.HistoryStorage](ctx) == nil {
|
||||
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
|
||||
}
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if platformInterface != nil {
|
||||
@@ -164,7 +192,7 @@ func New(options Options) (*Box, error) {
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI,
|
||||
Observable: needClashAPI || needAPIService,
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
@@ -175,6 +203,7 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[log.Factory](ctx, logFactory)
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
@@ -187,8 +216,12 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
netnsManager, err := netns.NewManager(logFactory.NewLogger("netns"), options.NetworkNamespaces, options.NetworkNamespaceHolderArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.NetworkNamespaceManager](ctx, netnsManager)
|
||||
internalServices = append(internalServices, netnsManager)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
@@ -196,14 +229,20 @@ func New(options Options) (*Box, error) {
|
||||
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS router")
|
||||
}
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
@@ -211,12 +250,22 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
router := route.NewRouter(ctx, logFactory, "router", routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
if needClashAPI || needAPIService {
|
||||
trafficManager := trafficcontrol.NewManager(outboundManager)
|
||||
service.MustRegisterPtr(ctx, trafficManager)
|
||||
router.AppendTracker(trafficManager)
|
||||
internalServices = append(internalServices, trafficManager)
|
||||
}
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
@@ -294,6 +343,24 @@ func New(options Options) (*Box, error) {
|
||||
Tag: "Compatible",
|
||||
Type: C.TypeDirect,
|
||||
})
|
||||
for i, serviceOptions := range options.Services {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
@@ -320,22 +387,41 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
for i, providerOptions := range options.Providers {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
if providerOptions.Tag != "" {
|
||||
tag = providerOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
err = providerManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
router,
|
||||
logFactory,
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
providerOptions.Type,
|
||||
providerOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
return nil, E.Cause(err, "initialize provider[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, certificateProviderOptions := range options.CertificateProviders {
|
||||
var tag string
|
||||
if certificateProviderOptions.Tag != "" {
|
||||
tag = certificateProviderOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = certificateProviderManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
certificateProviderOptions.Type,
|
||||
certificateProviderOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||
@@ -356,6 +442,12 @@ func New(options Options) (*Box, error) {
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
httpClientManager.Initialize(func() (*httpclient.ManagedTransport, error) {
|
||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
||||
var httpClientOptions option.HTTPClientOptions
|
||||
httpClientOptions.DefaultOutbound = true
|
||||
return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
@@ -363,7 +455,7 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
@@ -374,7 +466,6 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
}
|
||||
router.AppendTracker(clashServer)
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
@@ -390,6 +481,12 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
}
|
||||
if ntpOptions.Enabled {
|
||||
if ntpOptions.WriteToSystem {
|
||||
err = adapter.CheckSecurityFeature(ctx, "NTP `write_to_system`")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create NTP service")
|
||||
@@ -406,21 +503,25 @@ func New(options Options) (*Box, error) {
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
provider: providerManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
provider: providerManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
certificateProvider: certificateProviderManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
debugOptions: debugOptions,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -439,7 +540,7 @@ func (s *Box) PreStart() error {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
s.logger.Notice("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -458,7 +559,7 @@ func (s *Box) Start() error {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
s.logger.Notice("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -470,15 +571,28 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "start logger")
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
applyDebugOptions(s.debugOptions)
|
||||
s.debugHTTPServer, err = startDebugHTTPServer(s.debugOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.provider, s.inbound, s.endpoint, s.service, s.certificateProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.network, s.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -490,27 +604,35 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.provider, s.service)
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.certificateProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStatePostStart, s.outbound, s.provider, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStatePostStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.provider, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStarted, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -525,14 +647,21 @@ func (s *Box) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
var err error
|
||||
if s.debugHTTPServer != nil {
|
||||
err = E.Append(err, s.debugHTTPServer.Close(), func(err error) error {
|
||||
return E.Cause(err, "close debug HTTP server")
|
||||
})
|
||||
s.debugHTTPServer = nil
|
||||
}
|
||||
for _, closeItem := range []struct {
|
||||
name string
|
||||
service adapter.Lifecycle
|
||||
}{
|
||||
{"service", s.service},
|
||||
{"endpoint", s.endpoint},
|
||||
{"inbound", s.inbound},
|
||||
{"certificate-provider", s.certificateProvider},
|
||||
{"provider", s.provider},
|
||||
{"endpoint", s.endpoint},
|
||||
{"outbound", s.outbound},
|
||||
{"router", s.router},
|
||||
{"connection", s.connection},
|
||||
@@ -546,6 +675,14 @@ func (s *Box) Close() error {
|
||||
})
|
||||
done()
|
||||
}
|
||||
if s.httpClientService != nil {
|
||||
s.logger.Trace("close ", s.httpClientService.Name())
|
||||
startTime := time.Now()
|
||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
||||
})
|
||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
for _, lifecycleService := range s.internalService {
|
||||
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
|
||||
@@ -19,8 +19,11 @@ import (
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
switch os.Args[1] {
|
||||
case "next_macos_project_version":
|
||||
err := fetchMacOSVersion(ctx)
|
||||
case "next_project_version":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := fetchNextProjectVersion(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -50,8 +53,8 @@ func main() {
|
||||
}
|
||||
|
||||
const (
|
||||
appID = "6673731168"
|
||||
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
|
||||
appID = "6785326793"
|
||||
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
|
||||
)
|
||||
|
||||
func createClient(expireDuration time.Duration) *asc.Client {
|
||||
@@ -66,36 +69,49 @@ func createClient(expireDuration time.Duration) *asc.Client {
|
||||
return asc.NewClient(tokenConfig.Client())
|
||||
}
|
||||
|
||||
func fetchMacOSVersion(ctx context.Context) error {
|
||||
func fetchNextProjectVersion(ctx context.Context, platformName string) error {
|
||||
var platform asc.Platform
|
||||
switch platformName {
|
||||
case "ios":
|
||||
platform = asc.PlatformIOS
|
||||
case "macos":
|
||||
platform = asc.PlatformMACOS
|
||||
case "tvos":
|
||||
platform = asc.PlatformTVOS
|
||||
default:
|
||||
return E.New("unknown platform: ", platformName)
|
||||
}
|
||||
|
||||
query := &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
Limit: 200,
|
||||
}
|
||||
if platform != asc.PlatformMACOS {
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query.FilterPreReleaseVersionVersion = []string{build_shared.TestFlightVersion(tagVersion)}
|
||||
}
|
||||
|
||||
client := createClient(time.Minute)
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{"MAC_OS"},
|
||||
})
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var versionID string
|
||||
findVersion:
|
||||
for _, version := range versions.Data {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStateReadyForSale,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
versionID = version.ID
|
||||
break findVersion
|
||||
nextProjectVersion := 1
|
||||
var projectVersion int
|
||||
for _, build := range builds.Data {
|
||||
projectVersion, err = strconv.Atoi(*build.Attributes.Version)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse version code")
|
||||
}
|
||||
if projectVersion >= nextProjectVersion {
|
||||
nextProjectVersion = projectVersion + 1
|
||||
}
|
||||
}
|
||||
if versionID == "" {
|
||||
return E.New("no version found")
|
||||
}
|
||||
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse version code")
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
|
||||
os.Stdout.WriteString(F.ToString(nextProjectVersion, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -289,7 +305,7 @@ func prepareAppStore(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Fatal(platform, " ", tag, " no build found")
|
||||
log.Fatal(string(platform), " ", tag, " no build found")
|
||||
}
|
||||
buildID := common.Ptr(builds.Data[0].ID)
|
||||
if version.ID == "" {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
os.Setenv("GOPATH", build.Default.GOPATH)
|
||||
}
|
||||
|
||||
command := exec.Command(os.Args[1], os.Args[2:]...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err := command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/windivert"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-usbip/driverassets"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var (
|
||||
debugEnabled bool
|
||||
outputPath string
|
||||
target string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||
flag.StringVar(&outputPath, "output", "", "output path")
|
||||
flag.StringVar(&target, "target", runtime.GOOS+"/"+runtime.GOARCH, "target platform")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
err := build()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func build() error {
|
||||
targetParts := strings.Split(target, "/")
|
||||
if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" {
|
||||
return E.New("invalid target: ", target)
|
||||
}
|
||||
operatingSystem := targetParts[0]
|
||||
architecture := targetParts[1]
|
||||
if outputPath == "" {
|
||||
outputPath = "sing-box-daemon"
|
||||
if operatingSystem == "windows" {
|
||||
outputPath += ".exe"
|
||||
}
|
||||
}
|
||||
absoluteOutputPath, err := filepath.Abs(outputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "resolve output path")
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create output directory")
|
||||
}
|
||||
version, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return E.Cause(err, "read version")
|
||||
}
|
||||
cgoEnabled := operatingSystem != "windows" && os.Getenv("CC") != ""
|
||||
tags, err := buildTags(operatingSystem, architecture, cgoEnabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arguments := []string{
|
||||
"build",
|
||||
"-v",
|
||||
"-trimpath",
|
||||
"-buildvcs=false",
|
||||
"-tags", strings.Join(tags, ","),
|
||||
"-ldflags", build_shared.LinkerFlags(version, debugEnabled),
|
||||
"-o", absoluteOutputPath,
|
||||
}
|
||||
if operatingSystem == "windows" && architecture == "386" {
|
||||
arguments = append(arguments, "-gcflags=net=-l")
|
||||
}
|
||||
arguments = append(arguments, "./experimental/boxdd")
|
||||
command := exec.Command("go", arguments...)
|
||||
cgoEnabledValue := "0"
|
||||
if cgoEnabled {
|
||||
cgoEnabledValue = "1"
|
||||
}
|
||||
command.Env = append(os.Environ(),
|
||||
"CGO_ENABLED="+cgoEnabledValue,
|
||||
"GOOS="+operatingSystem,
|
||||
"GOARCH="+architecture,
|
||||
"GOTOOLCHAIN=local",
|
||||
)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err = command.Run()
|
||||
if err != nil {
|
||||
return E.Cause(err, "build sing-box daemon")
|
||||
}
|
||||
if operatingSystem == "windows" {
|
||||
err = stageWinDivertDriver(architecture, filepath.Dir(absoluteOutputPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stageUSBIPDrivers(architecture, filepath.Dir(absoluteOutputPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stageUSBIPDrivers(architecture string, outputDirectory string) error {
|
||||
driverPackages := []struct {
|
||||
assets map[string]driverassets.Package
|
||||
assetDir string
|
||||
}{
|
||||
{driverassets.VBoxUSB, filepath.Join("internal", "vboxusb", "assets")},
|
||||
{driverassets.VHCI, filepath.Join("internal", "usbipvhci", "assets")},
|
||||
}
|
||||
var moduleDirectory string
|
||||
for _, driverPackage := range driverPackages {
|
||||
staged := driverPackage.assets[architecture]
|
||||
for _, architecturePackage := range driverPackage.assets {
|
||||
for _, file := range architecturePackage.Files {
|
||||
if slices.ContainsFunc(staged.Files, func(stagedFile driverassets.File) bool {
|
||||
return stagedFile.Name == file.Name
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
err := os.Remove(filepath.Join(outputDirectory, file.Name))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return E.Cause(err, "remove stale ", file.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(staged.Files) == 0 {
|
||||
continue
|
||||
}
|
||||
if moduleDirectory == "" {
|
||||
listOutput, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/sagernet/sing-usbip").Output()
|
||||
if err != nil {
|
||||
return E.Cause(err, "locate sing-usbip module directory")
|
||||
}
|
||||
moduleDirectory = strings.TrimSpace(string(listOutput))
|
||||
}
|
||||
for _, file := range staged.Files {
|
||||
content, err := os.ReadFile(filepath.Join(moduleDirectory, driverPackage.assetDir, architecture, file.Name))
|
||||
if err != nil {
|
||||
return E.Cause(err, "read ", file.Name)
|
||||
}
|
||||
checksum := sha256.Sum256(content)
|
||||
if hex.EncodeToString(checksum[:]) != file.SHA256 {
|
||||
return E.New(file.Name, " does not match the digest declared in sing-usbip/driverassets")
|
||||
}
|
||||
targetPath := filepath.Join(outputDirectory, file.Name)
|
||||
stagedContent, err := os.ReadFile(targetPath)
|
||||
if err == nil && bytes.Equal(stagedContent, content) {
|
||||
continue
|
||||
}
|
||||
err = os.WriteFile(targetPath, content, 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write ", file.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stageWinDivertDriver(architecture string, outputDirectory string) error {
|
||||
var assetName, assetDigest string
|
||||
switch architecture {
|
||||
case "amd64":
|
||||
assetName, assetDigest = windivert.Asset64Name, windivert.Asset64SHA256
|
||||
case "386":
|
||||
assetName, assetDigest = windivert.Asset32Name, windivert.Asset32SHA256
|
||||
}
|
||||
for _, name := range []string{windivert.Asset64Name, windivert.Asset32Name} {
|
||||
if name == assetName {
|
||||
continue
|
||||
}
|
||||
err := os.Remove(filepath.Join(outputDirectory, name))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return E.Cause(err, "remove stale ", name)
|
||||
}
|
||||
}
|
||||
if assetName == "" {
|
||||
return nil
|
||||
}
|
||||
assetDirectory := filepath.Join("common", "windivert", "assets")
|
||||
content, err := os.ReadFile(filepath.Join(assetDirectory, assetName))
|
||||
if err != nil {
|
||||
return E.Cause(err, "read ", assetName)
|
||||
}
|
||||
checksum := sha256.Sum256(content)
|
||||
if hex.EncodeToString(checksum[:]) != assetDigest {
|
||||
return E.New(assetName, " does not match the digest declared in common/windivert")
|
||||
}
|
||||
targetPath := filepath.Join(outputDirectory, assetName)
|
||||
staged, err := os.ReadFile(targetPath)
|
||||
if err == nil && bytes.Equal(staged, content) {
|
||||
return nil
|
||||
}
|
||||
err = os.WriteFile(targetPath, content, 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write ", assetName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildTags(operatingSystem string, architecture string, cgoEnabled bool) ([]string, error) {
|
||||
tagsFile := "release/DEFAULT_BUILD_TAGS"
|
||||
if operatingSystem == "windows" {
|
||||
if architecture == "386" {
|
||||
tagsFile = "release/DEFAULT_BUILD_TAGS_OTHERS"
|
||||
} else {
|
||||
tagsFile = "release/DEFAULT_BUILD_TAGS_WINDOWS"
|
||||
}
|
||||
} else if !cgoEnabled {
|
||||
tagsFile = "release/DEFAULT_BUILD_TAGS_OTHERS"
|
||||
}
|
||||
content, err := os.ReadFile(tagsFile)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read build tags")
|
||||
}
|
||||
tags := strings.Split(strings.TrimSpace(string(content)), ",")
|
||||
if operatingSystem == "windows" {
|
||||
tags = append(tags, "with_external_windivert", "with_external_usbip_drivers")
|
||||
}
|
||||
if debugEnabled {
|
||||
tags = append(tags, "debug")
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
@@ -60,11 +60,13 @@ func init() {
|
||||
if err != nil {
|
||||
currentTag = "unknown"
|
||||
}
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
|
||||
sharedFlags = append(sharedFlags, "-ldflags", build_shared.LinkerFlags(currentTag, false))
|
||||
debugFlags = append(debugFlags, "-ldflags", build_shared.LinkerFlags(currentTag, true))
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_utls", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "grpcnotrace")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_trusttunnel", "with_call", "with_sudoku", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "with_openvpn", "with_openconnect", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
@@ -161,20 +163,20 @@ func buildAndroid() {
|
||||
|
||||
bindTarget := getAndroidBindTarget()
|
||||
|
||||
// Build main variant (SDK 23)
|
||||
// Build main variant (SDK 24)
|
||||
mainTags := append([]string{}, sharedTags...)
|
||||
// mainTags = append(mainTags, memcTags...)
|
||||
if debugEnabled {
|
||||
mainTags = append(mainTags, debugTags...)
|
||||
}
|
||||
buildAndroidVariant(AndroidBuildConfig{
|
||||
AndroidAPI: 23,
|
||||
AndroidAPI: 24,
|
||||
OutputName: "libbox.aar",
|
||||
Tags: mainTags,
|
||||
}, bindTarget)
|
||||
|
||||
// Build legacy variant (SDK 21)
|
||||
legacyTags := append([]string{}, sharedTags...)
|
||||
// Build legacy variant (SDK 21, no naive outbound)
|
||||
legacyTags := filterTags(sharedTags, "with_naive_outbound")
|
||||
// legacyTags = append(legacyTags, memcTags...)
|
||||
if debugEnabled {
|
||||
legacyTags = append(legacyTags, debugTags...)
|
||||
@@ -202,6 +204,9 @@ func buildApple() {
|
||||
"-target", bindTarget,
|
||||
"-libname=box",
|
||||
"-tags-not-macos=with_low_memory",
|
||||
"-iosversion=15.0",
|
||||
"-macosversion=13.0",
|
||||
"-tvosversion=17.0",
|
||||
}
|
||||
//if !withTailscale {
|
||||
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package build_shared
|
||||
|
||||
import "strings"
|
||||
|
||||
func LinkerFlags(version string, debug bool) string {
|
||||
flags := []string{
|
||||
"-X github.com/sagernet/sing-box/constant.Version=" + version,
|
||||
"-X runtime.godebugDefault=multipathtcp=0,tlssha1=1",
|
||||
"-checklinkname=0",
|
||||
}
|
||||
if !debug {
|
||||
flags = append(flags, "-s", "-w", "-buildid=")
|
||||
}
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package build_shared
|
||||
import (
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing/common"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
)
|
||||
|
||||
@@ -36,3 +37,7 @@ func ReadTagVersion() (badversion.Version, error) {
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func TestFlightVersion(version badversion.Version) string {
|
||||
return F.ToString(version.Major, ".", version.Minor, ".10")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var outputPath string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&outputPath, "output", "", "output AAR path")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
err := merge()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func merge() error {
|
||||
inputPaths := flag.Args()
|
||||
if outputPath == "" {
|
||||
return E.New("missing output path")
|
||||
}
|
||||
if len(inputPaths) == 0 {
|
||||
return E.New("missing input AAR paths")
|
||||
}
|
||||
archiveReaders := make([]*zip.ReadCloser, 0, len(inputPaths))
|
||||
for _, inputPath := range inputPaths {
|
||||
archiveReader, err := zip.OpenReader(inputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open input AAR: ", inputPath)
|
||||
}
|
||||
archiveReaders = append(archiveReaders, archiveReader)
|
||||
}
|
||||
defer func() {
|
||||
for _, archiveReader := range archiveReaders {
|
||||
archiveReader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
referenceEntries := make(map[string][sha256.Size]byte)
|
||||
selectedEntries := make([]*zip.File, 0)
|
||||
selectedJNIEntries := make(map[string]bool)
|
||||
for inputIndex, archiveReader := range archiveReaders {
|
||||
seenEntries := make(map[string]bool)
|
||||
for _, archiveFile := range archiveReader.File {
|
||||
if strings.HasPrefix(archiveFile.Name, "jni/") {
|
||||
if archiveFile.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
if selectedJNIEntries[archiveFile.Name] {
|
||||
return E.New("duplicate AAR JNI entry: ", archiveFile.Name)
|
||||
}
|
||||
selectedJNIEntries[archiveFile.Name] = true
|
||||
selectedEntries = append(selectedEntries, archiveFile)
|
||||
continue
|
||||
}
|
||||
entryDigest, err := digestEntry(archiveFile)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read AAR entry: ", archiveFile.Name)
|
||||
}
|
||||
if inputIndex == 0 {
|
||||
referenceEntries[archiveFile.Name] = entryDigest
|
||||
selectedEntries = append(selectedEntries, archiveFile)
|
||||
} else {
|
||||
referenceDigest, loaded := referenceEntries[archiveFile.Name]
|
||||
if !loaded {
|
||||
return E.New("unexpected AAR entry: ", archiveFile.Name)
|
||||
}
|
||||
if referenceDigest != entryDigest {
|
||||
return E.New("AAR entry differs between architectures: ", archiveFile.Name)
|
||||
}
|
||||
}
|
||||
seenEntries[archiveFile.Name] = true
|
||||
}
|
||||
if inputIndex > 0 {
|
||||
for referenceName := range referenceEntries {
|
||||
if !seenEntries[referenceName] {
|
||||
return E.New("missing AAR entry: ", referenceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
absoluteOutputPath, err := filepath.Abs(outputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "resolve output AAR path")
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create output AAR directory")
|
||||
}
|
||||
temporaryFile, err := os.CreateTemp(filepath.Dir(absoluteOutputPath), ".merge-aar-*.aar")
|
||||
if err != nil {
|
||||
return E.Cause(err, "create temporary output AAR")
|
||||
}
|
||||
temporaryPath := temporaryFile.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
archiveWriter := zip.NewWriter(temporaryFile)
|
||||
for _, archiveFile := range selectedEntries {
|
||||
rawReader, openErr := archiveFile.OpenRaw()
|
||||
if openErr != nil {
|
||||
archiveWriter.Close()
|
||||
temporaryFile.Close()
|
||||
return E.Cause(openErr, "open raw AAR entry: ", archiveFile.Name)
|
||||
}
|
||||
header := archiveFile.FileHeader
|
||||
rawWriter, createErr := archiveWriter.CreateRaw(&header)
|
||||
if createErr != nil {
|
||||
archiveWriter.Close()
|
||||
temporaryFile.Close()
|
||||
return E.Cause(createErr, "create output AAR entry: ", archiveFile.Name)
|
||||
}
|
||||
_, copyErr := io.Copy(rawWriter, rawReader)
|
||||
if copyErr != nil {
|
||||
archiveWriter.Close()
|
||||
temporaryFile.Close()
|
||||
return E.Cause(copyErr, "copy output AAR entry: ", archiveFile.Name)
|
||||
}
|
||||
}
|
||||
err = archiveWriter.Close()
|
||||
if err != nil {
|
||||
temporaryFile.Close()
|
||||
return E.Cause(err, "finalize output AAR")
|
||||
}
|
||||
err = temporaryFile.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close output AAR")
|
||||
}
|
||||
err = os.Rename(temporaryPath, absoluteOutputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "replace output AAR")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func digestEntry(archiveFile *zip.File) ([sha256.Size]byte, error) {
|
||||
entryReader, err := archiveFile.Open()
|
||||
if err != nil {
|
||||
return [sha256.Size]byte{}, err
|
||||
}
|
||||
digest := sha256.New()
|
||||
_, err = io.Copy(digest, entryReader)
|
||||
closeErr := entryReader.Close()
|
||||
if err != nil {
|
||||
return [sha256.Size]byte{}, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return [sha256.Size]byte{}, closeErr
|
||||
}
|
||||
var result [sha256.Size]byte
|
||||
copy(result[:], digest.Sum(nil))
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
type xcFrameworkInfo struct {
|
||||
AvailableLibraries []xcFrameworkLibrary `plist:"AvailableLibraries"`
|
||||
}
|
||||
|
||||
type xcFrameworkLibrary struct {
|
||||
BinaryPath string `plist:"BinaryPath"`
|
||||
LibraryIdentifier string `plist:"LibraryIdentifier"`
|
||||
LibraryPath string `plist:"LibraryPath"`
|
||||
SupportedArchitectures []string `plist:"SupportedArchitectures"`
|
||||
SupportedPlatform string `plist:"SupportedPlatform"`
|
||||
SupportedPlatformVariant string `plist:"SupportedPlatformVariant"`
|
||||
}
|
||||
|
||||
type frameworkSlice struct {
|
||||
rootPath string
|
||||
library xcFrameworkLibrary
|
||||
}
|
||||
|
||||
var outputPath string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&outputPath, "output", "", "output XCFramework path")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
err := merge()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func merge() error {
|
||||
inputPaths := flag.Args()
|
||||
if outputPath == "" {
|
||||
return E.New("missing output path")
|
||||
}
|
||||
if len(inputPaths) == 0 {
|
||||
return E.New("missing input XCFramework paths")
|
||||
}
|
||||
frameworkGroups := make(map[string][]frameworkSlice)
|
||||
for _, inputPath := range inputPaths {
|
||||
infoFile, err := os.Open(filepath.Join(inputPath, "Info.plist"))
|
||||
if err != nil {
|
||||
return E.Cause(err, "open XCFramework metadata: ", inputPath)
|
||||
}
|
||||
var info xcFrameworkInfo
|
||||
decoder := plist.NewDecoder(infoFile)
|
||||
err = decoder.Decode(&info)
|
||||
closeErr := infoFile.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "decode XCFramework metadata: ", inputPath)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return E.Cause(closeErr, "close XCFramework metadata: ", inputPath)
|
||||
}
|
||||
for _, library := range info.AvailableLibraries {
|
||||
groupName := library.SupportedPlatform + "|" + library.SupportedPlatformVariant
|
||||
frameworkGroups[groupName] = append(frameworkGroups[groupName], frameworkSlice{
|
||||
rootPath: inputPath,
|
||||
library: library,
|
||||
})
|
||||
}
|
||||
}
|
||||
groupNames := make([]string, 0, len(frameworkGroups))
|
||||
for groupName := range frameworkGroups {
|
||||
groupNames = append(groupNames, groupName)
|
||||
}
|
||||
sort.Strings(groupNames)
|
||||
absoluteOutputPath, err := filepath.Abs(outputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "resolve output XCFramework path")
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create output XCFramework directory")
|
||||
}
|
||||
temporaryDirectory, err := os.MkdirTemp(filepath.Dir(absoluteOutputPath), ".merge-xcframework-*")
|
||||
if err != nil {
|
||||
return E.Cause(err, "create XCFramework merge directory")
|
||||
}
|
||||
defer os.RemoveAll(temporaryDirectory)
|
||||
frameworkPaths := make([]string, 0, len(groupNames))
|
||||
for groupIndex, groupName := range groupNames {
|
||||
frameworkSlices := frameworkGroups[groupName]
|
||||
firstSlice := frameworkSlices[0]
|
||||
firstFrameworkPath := filepath.Join(firstSlice.rootPath, firstSlice.library.LibraryIdentifier, firstSlice.library.LibraryPath)
|
||||
if len(frameworkSlices) == 1 {
|
||||
frameworkPaths = append(frameworkPaths, firstFrameworkPath)
|
||||
continue
|
||||
}
|
||||
architectures := make(map[string]bool)
|
||||
binaryPaths := make([]string, 0, len(frameworkSlices))
|
||||
for _, currentSlice := range frameworkSlices {
|
||||
if currentSlice.library.LibraryPath != firstSlice.library.LibraryPath || currentSlice.library.BinaryPath != firstSlice.library.BinaryPath {
|
||||
return E.New("incompatible XCFramework slices for platform: ", currentSlice.library.SupportedPlatform)
|
||||
}
|
||||
for _, architecture := range currentSlice.library.SupportedArchitectures {
|
||||
if architectures[architecture] {
|
||||
return E.New("duplicate XCFramework architecture: ", architecture)
|
||||
}
|
||||
architectures[architecture] = true
|
||||
}
|
||||
binaryPaths = append(binaryPaths, filepath.Join(currentSlice.rootPath, currentSlice.library.LibraryIdentifier, currentSlice.library.BinaryPath))
|
||||
}
|
||||
mergedFrameworkPath := filepath.Join(temporaryDirectory, "framework-"+strconv.Itoa(groupIndex), filepath.Base(firstSlice.library.LibraryPath))
|
||||
copyCommand := exec.Command("ditto", firstFrameworkPath, mergedFrameworkPath)
|
||||
copyCommand.Stdout = os.Stdout
|
||||
copyCommand.Stderr = os.Stderr
|
||||
err = copyCommand.Run()
|
||||
if err != nil {
|
||||
return E.Cause(err, "copy XCFramework slice")
|
||||
}
|
||||
binaryRelativePath, relativeErr := filepath.Rel(firstSlice.library.LibraryPath, firstSlice.library.BinaryPath)
|
||||
if relativeErr != nil {
|
||||
return E.Cause(relativeErr, "resolve XCFramework binary path")
|
||||
}
|
||||
if binaryRelativePath == "." || strings.HasPrefix(binaryRelativePath, ".."+string(filepath.Separator)) {
|
||||
return E.New("invalid XCFramework binary path: ", firstSlice.library.BinaryPath)
|
||||
}
|
||||
mergedBinaryPath := filepath.Join(mergedFrameworkPath, binaryRelativePath)
|
||||
temporaryBinaryPath := mergedBinaryPath + ".merged"
|
||||
lipoArguments := append([]string{"lipo", "-create"}, binaryPaths...)
|
||||
lipoArguments = append(lipoArguments, "-output", temporaryBinaryPath)
|
||||
lipoCommand := exec.Command("xcrun", lipoArguments...)
|
||||
lipoCommand.Stdout = os.Stdout
|
||||
lipoCommand.Stderr = os.Stderr
|
||||
err = lipoCommand.Run()
|
||||
if err != nil {
|
||||
return E.Cause(err, "merge XCFramework binaries")
|
||||
}
|
||||
err = os.Rename(temporaryBinaryPath, mergedBinaryPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "replace merged XCFramework binary")
|
||||
}
|
||||
frameworkPaths = append(frameworkPaths, mergedFrameworkPath)
|
||||
}
|
||||
err = os.RemoveAll(absoluteOutputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "remove output XCFramework")
|
||||
}
|
||||
xcodebuildArguments := []string{"-create-xcframework"}
|
||||
for _, frameworkPath := range frameworkPaths {
|
||||
xcodebuildArguments = append(xcodebuildArguments, "-framework", frameworkPath)
|
||||
}
|
||||
xcodebuildArguments = append(xcodebuildArguments, "-output", absoluteOutputPath)
|
||||
xcodebuildCommand := exec.Command("xcodebuild", xcodebuildArguments...)
|
||||
xcodebuildCommand.Stdout = os.Stdout
|
||||
xcodebuildCommand.Stderr = os.Stderr
|
||||
err = xcodebuildCommand.Run()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create XCFramework")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -48,8 +48,8 @@ func GetRuntimeEnv(key string) (string, error) {
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
envStrings := strings.Split(string(data), "\n")
|
||||
for _, envItem := range envStrings {
|
||||
envStrings := strings.SplitSeq(string(data), "\n")
|
||||
for envItem := range envStrings {
|
||||
envItem = strings.TrimSuffix(envItem, "\r")
|
||||
envKeyValue := strings.Split(envItem, "=")
|
||||
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
|
||||
|
||||
@@ -39,7 +39,7 @@ func main() {
|
||||
common.Must(os.Chdir(androidPath))
|
||||
localProps := common.Must1(os.ReadFile("version.properties"))
|
||||
var propsList [][]string
|
||||
for _, propLine := range strings.Split(string(localProps), "\n") {
|
||||
for propLine := range strings.SplitSeq(string(localProps), "\n") {
|
||||
propsList = append(propsList, strings.Split(propLine, "="))
|
||||
}
|
||||
var (
|
||||
|
||||
@@ -8,16 +8,21 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
var flagRunInCI bool
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagTestFlight bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagTestFlight, "testflight", false, "Override the App Store marketing version with the version reserved for TestFlight")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -40,19 +45,39 @@ func main() {
|
||||
common.Must(decoder.Decode(&project))
|
||||
objectsMap := project["objects"].(map[string]any)
|
||||
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
|
||||
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString())
|
||||
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String())
|
||||
if updated0 || updated1 {
|
||||
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
|
||||
}
|
||||
var updated2 bool
|
||||
if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" {
|
||||
newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion)
|
||||
if updated2 {
|
||||
log.Info("updated macos project version to ", macProjectVersion)
|
||||
newContent := projectContent
|
||||
var marketingVersionUpdated bool
|
||||
if flagTestFlight {
|
||||
testFlightVersion := build_shared.TestFlightVersion(newVersion)
|
||||
newContent, marketingVersionUpdated = findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfamt"}, testFlightVersion)
|
||||
if marketingVersionUpdated {
|
||||
log.Info("updated App Store version to ", testFlightVersion)
|
||||
}
|
||||
}
|
||||
if updated0 || updated1 || updated2 {
|
||||
var standaloneVersionUpdated bool
|
||||
newContent, standaloneVersionUpdated = findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfamt.standalone", "io.nekohasekai.sfamt.system"}, newVersion.String())
|
||||
if standaloneVersionUpdated {
|
||||
marketingVersionUpdated = true
|
||||
log.Info("updated version to ", newVersion.String())
|
||||
}
|
||||
var projectVersionUpdated bool
|
||||
for environmentName, directory := range map[string]string{
|
||||
"IOS_PROJECT_VERSION": "SFI",
|
||||
"MACOS_PROJECT_VERSION": "SFM",
|
||||
"TVOS_PROJECT_VERSION": "SFT",
|
||||
} {
|
||||
projectVersion := os.Getenv(environmentName)
|
||||
if projectVersion == "" {
|
||||
continue
|
||||
}
|
||||
var updated bool
|
||||
newContent, updated = findAndReplaceProjectVersion(objectsMap, newContent, []string{directory}, projectVersion)
|
||||
if updated {
|
||||
projectVersionUpdated = true
|
||||
log.Info("updated ", directory, " project version to ", projectVersion)
|
||||
}
|
||||
}
|
||||
if marketingVersionUpdated || projectVersionUpdated {
|
||||
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
|
||||
}
|
||||
}
|
||||
@@ -76,11 +101,20 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
|
||||
projectContent = projectContent[:versionStart] + formatProjectVersion(newVersion) + projectContent[versionEnd:]
|
||||
}
|
||||
return projectContent, updated
|
||||
}
|
||||
|
||||
// Xcode serializes a version without quotes unless it contains a pre-release
|
||||
// part; always quoting makes Xcode rewrite the value on the next save.
|
||||
func formatProjectVersion(version string) string {
|
||||
if badversion.Parse(version).PreReleaseIdentifier == "" {
|
||||
return version
|
||||
}
|
||||
return "\"" + version + "\""
|
||||
}
|
||||
|
||||
func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) {
|
||||
objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList)
|
||||
var updated bool
|
||||
@@ -106,6 +140,7 @@ func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent stri
|
||||
}
|
||||
|
||||
func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
|
||||
globalSettings := collectBuildSettings(objectsMap)
|
||||
var objectKeyList []string
|
||||
for objectKey, object := range objectsMap {
|
||||
buildSettings := object.(map[string]any)["buildSettings"]
|
||||
@@ -116,13 +151,51 @@ func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
|
||||
if bundleIDObject == nil {
|
||||
continue
|
||||
}
|
||||
if common.Contains(bundleIDList, bundleIDObject.(string)) {
|
||||
bundleID := expandBuildVariables(bundleIDObject.(string), globalSettings)
|
||||
if common.Contains(bundleIDList, bundleID) {
|
||||
objectKeyList = append(objectKeyList, objectKey)
|
||||
}
|
||||
}
|
||||
return objectKeyList
|
||||
}
|
||||
|
||||
func collectBuildSettings(objectsMap map[string]any) map[string]string {
|
||||
settings := make(map[string]string)
|
||||
for _, object := range objectsMap {
|
||||
buildSettings, loaded := object.(map[string]any)["buildSettings"].(map[string]any)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
for key, value := range buildSettings {
|
||||
valueString, isString := value.(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
settings[key] = valueString
|
||||
}
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
var buildVariableRegexp = regexp.MustCompile(`\$[({]([A-Za-z0-9_]+)[)}]`)
|
||||
|
||||
func expandBuildVariables(value string, settings map[string]string) string {
|
||||
for {
|
||||
expanded := buildVariableRegexp.ReplaceAllStringFunc(value, func(match string) string {
|
||||
name := buildVariableRegexp.FindStringSubmatch(match)[1]
|
||||
replacement, loaded := settings[name]
|
||||
if !loaded {
|
||||
return match
|
||||
}
|
||||
return replacement
|
||||
})
|
||||
if expanded == value {
|
||||
return expanded
|
||||
}
|
||||
value = expanded
|
||||
}
|
||||
}
|
||||
|
||||
func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string {
|
||||
var objectKeyList []string
|
||||
for objectKey, object := range objectsMap {
|
||||
|
||||
@@ -43,11 +43,8 @@ func updateMozillaIncludedRootCAs() error {
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
func newMozillaIncluded() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
`)
|
||||
func mozillaIncludedPEM() string {
|
||||
return ` + "`")
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -58,17 +55,14 @@ func newMozillaIncluded() *x509.CertPool {
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
cert := strings.Trim(record[certIndex], "'")
|
||||
generated.WriteString("\n// ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" pool.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
generated.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("\treturn pool\n}\n")
|
||||
generated.WriteString("`\n}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
|
||||
@@ -127,11 +121,8 @@ func updateChromeIncludedRootCAs() error {
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
func newChromeIncluded() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
`)
|
||||
func chromeIncludedPEM() string {
|
||||
return ` + "`")
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -145,18 +136,13 @@ func newChromeIncluded() *x509.CertPool {
|
||||
if chinaFingerprints[record[fingerprintIndex]] {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
cert := strings.Trim(record[certIndex], "'")
|
||||
generated.WriteString("\n// ")
|
||||
generated.WriteString(record[subjectIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" pool.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes if present
|
||||
if len(cert) > 0 && cert[0] == '\'' {
|
||||
cert = cert[1 : len(cert)-1]
|
||||
}
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
generated.WriteString("\n")
|
||||
}
|
||||
generated.WriteString("\treturn pool\n}\n")
|
||||
generated.WriteString("`\n}\n")
|
||||
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
type versionMetadata struct {
|
||||
Version string `json:"version"`
|
||||
GoVersion string `json:"go_version"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
newVersion := common.Must1(build_shared.ReadTag())
|
||||
desktopPath := "../sing-box-for-desktop"
|
||||
if flagRunInCI {
|
||||
desktopPath = "clients/desktop"
|
||||
}
|
||||
desktopPath = common.Must1(filepath.Abs(desktopPath))
|
||||
versionPath := filepath.Join(desktopPath, "version.json")
|
||||
versionFile := common.Must1(os.Open(versionPath))
|
||||
var metadata versionMetadata
|
||||
common.Must(json.NewDecoder(versionFile).Decode(&metadata))
|
||||
common.Must(versionFile.Close())
|
||||
newGoVersion := runtime.Version()
|
||||
if match := regexp.MustCompile(`^go\d+\.\d+(?:\.\d+)?`).FindString(newGoVersion); match != "" {
|
||||
newGoVersion = match
|
||||
}
|
||||
versionUpdated := metadata.Version != newVersion
|
||||
goVersionUpdated := metadata.GoVersion != newGoVersion
|
||||
if !(versionUpdated || goVersionUpdated) {
|
||||
log.Info("version not changed")
|
||||
return
|
||||
}
|
||||
if versionUpdated {
|
||||
log.Info("updated version from ", metadata.Version, " to ", newVersion)
|
||||
}
|
||||
if goVersionUpdated {
|
||||
log.Info("updated Go version from ", metadata.GoVersion, " to ", newGoVersion)
|
||||
}
|
||||
if flagRunInCI && !flagRunNightly {
|
||||
log.Fatal("version changed, commit changes first.")
|
||||
}
|
||||
metadata.Version = newVersion
|
||||
metadata.GoVersion = newGoVersion
|
||||
outputFile := common.Must1(os.Create(versionPath))
|
||||
encoder := json.NewEncoder(outputFile)
|
||||
encoder.SetIndent("", " ")
|
||||
common.Must(encoder.Encode(metadata))
|
||||
common.Must(outputFile.Close())
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
|
||||
@@ -52,7 +53,9 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
|
||||
}
|
||||
if disableColor {
|
||||
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
||||
logFactory := log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false)
|
||||
common.Must(logFactory.Start())
|
||||
log.SetStdLogger(logFactory.Logger())
|
||||
}
|
||||
if workingDir != "" {
|
||||
_, err := os.Stat(workingDir)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAPIFlagURL string
|
||||
commandAPIFlagSecret string
|
||||
commandAPIServerURL string
|
||||
)
|
||||
|
||||
var commandAPI = &cobra.Command{
|
||||
Use: "api <command>",
|
||||
Short: "API service client",
|
||||
DisableFlagParsing: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := runAPI(args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
targetCommand, remainingArgs, err := commandAPIRoot.Find(args)
|
||||
if err != nil || len(remainingArgs) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return common.Map(common.Filter(targetCommand.Commands(), func(it *cobra.Command) bool {
|
||||
return it.IsAvailableCommand() && strings.HasPrefix(it.Name(), toComplete)
|
||||
}), func(it *cobra.Command) string {
|
||||
return it.Name() + "\t" + it.Short
|
||||
}), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
}
|
||||
|
||||
var commandAPIRoot = &cobra.Command{
|
||||
Use: "api",
|
||||
Short: "API service client",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIRoot.PersistentFlags().StringVar(&commandAPIFlagURL, "url", "", "API service URL (default: $BOX_API_URL)")
|
||||
commandAPIRoot.PersistentFlags().StringVar(&commandAPIFlagSecret, "secret", "", "API service secret (default: $BOX_API_SECRET)")
|
||||
mainCommand.AddCommand(commandAPI)
|
||||
}
|
||||
|
||||
func runAPI(args []string) error {
|
||||
commandAPIRoot.SetArgs(append([]string{}, args...))
|
||||
err := commandAPIRoot.Execute()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
grpcStatus, isStatus := status.FromError(err)
|
||||
if !isStatus {
|
||||
return err
|
||||
}
|
||||
switch grpcStatus.Code() {
|
||||
case codes.Unavailable:
|
||||
return E.New("failed to connect to API service at ", commandAPIServerURL, ": ", grpcStatus.Message())
|
||||
case codes.Unknown:
|
||||
return E.New(grpcStatus.Message())
|
||||
case codes.Unimplemented:
|
||||
return E.New(grpcStatus.Code().String(), ": ", grpcStatus.Message(), " (client API version ", daemon.APIVersion, ")")
|
||||
default:
|
||||
return E.New(grpcStatus.Code().String(), ": ", grpcStatus.Message())
|
||||
}
|
||||
}
|
||||
|
||||
func createAPIClient() (*grpc.ClientConn, daemon.StartedServiceClient, error) {
|
||||
serverURL := commandAPIFlagURL
|
||||
if serverURL == "" {
|
||||
serverURL = os.Getenv("BOX_API_URL")
|
||||
}
|
||||
if serverURL == "" {
|
||||
return nil, nil, E.New("missing API service URL, set --url or BOX_API_URL")
|
||||
}
|
||||
if !strings.Contains(serverURL, "://") {
|
||||
serverURL = "http://" + serverURL
|
||||
}
|
||||
commandAPIServerURL = serverURL
|
||||
secret := commandAPIFlagSecret
|
||||
if secret == "" {
|
||||
secret = os.Getenv("BOX_API_SECRET")
|
||||
}
|
||||
clientConn, err := daemon.NewRemoteClient(daemon.RemoteClientOptions{
|
||||
ServerURL: serverURL,
|
||||
Secret: secret,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return clientConn, daemon.NewStartedServiceClient(clientConn), nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIConnection = &cobra.Command{
|
||||
Use: "connection",
|
||||
Short: "Manage connections",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIRoot.AddCommand(commandAPIConnection)
|
||||
}
|
||||
|
||||
func fetchConnections(client daemon.StartedServiceClient) ([]*daemon.Connection, error) {
|
||||
stream, err := client.SubscribeConnections(globalCtx, &daemon.SubscribeConnectionsRequest{Interval: int64(time.Second)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := stream.Recv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connections := common.FilterNotNil(common.Map(events.GetEvents(), func(it *daemon.ConnectionEvent) *daemon.Connection {
|
||||
return it.GetConnection()
|
||||
}))
|
||||
common.SortBy(connections, func(it *daemon.Connection) int64 {
|
||||
return it.GetCreatedAt()
|
||||
})
|
||||
return connections, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var commandAPIConnectionCloseFlagAll bool
|
||||
|
||||
var commandAPIConnectionClose = &cobra.Command{
|
||||
Use: "close <id>",
|
||||
Short: "Close connections",
|
||||
Long: "Close connections.\n\nThe id must be a full UUID; the service reports success for an unknown or already closed connection.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIConnectionClose(args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIConnectionClose.Flags().BoolVar(&commandAPIConnectionCloseFlagAll, "all", false, "Close all connections")
|
||||
commandAPIConnection.AddCommand(commandAPIConnectionClose)
|
||||
}
|
||||
|
||||
func runAPIConnectionClose(args []string) error {
|
||||
if commandAPIConnectionCloseFlagAll {
|
||||
if len(args) > 0 {
|
||||
return E.New("--all takes no connection id")
|
||||
}
|
||||
} else if len(args) == 0 {
|
||||
return E.New("missing connection id")
|
||||
}
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
if commandAPIConnectionCloseFlagAll {
|
||||
_, err = client.CloseAllConnections(globalCtx, &emptypb.Empty{})
|
||||
} else {
|
||||
_, err = client.CloseConnection(globalCtx, &daemon.CloseConnectionRequest{Id: args[0]})
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIConnectionListFlagColumns []string
|
||||
|
||||
type connectionRate struct {
|
||||
uplink int64
|
||||
downlink int64
|
||||
}
|
||||
|
||||
type connectionColumn struct {
|
||||
header string
|
||||
value func(connection *daemon.Connection, rates map[string]connectionRate) string
|
||||
}
|
||||
|
||||
var connectionColumnNames = []string{
|
||||
"id", "network", "source", "destination", "inbound", "outbound",
|
||||
"chain", "rule", "protocol", "user", "process", "created", "rate", "total",
|
||||
}
|
||||
|
||||
var connectionColumns = map[string]connectionColumn{
|
||||
"id": {"ID", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetId()
|
||||
}},
|
||||
"network": {"NETWORK", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetNetwork()
|
||||
}},
|
||||
"source": {"SOURCE", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetSource()
|
||||
}},
|
||||
"destination": {"DESTINATION", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connectionDestination(connection)
|
||||
}},
|
||||
"inbound": {"INBOUND", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
if connection.GetInbound() == "" {
|
||||
return connection.GetInboundType()
|
||||
}
|
||||
return connection.GetInboundType() + "/" + connection.GetInbound()
|
||||
}},
|
||||
"outbound": {"OUTBOUND", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetOutbound()
|
||||
}},
|
||||
"chain": {"CHAIN", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
chain := slices.Clone(connection.GetChainList())
|
||||
slices.Reverse(chain)
|
||||
return strings.Join(chain, "/")
|
||||
}},
|
||||
"rule": {"RULE", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetRule()
|
||||
}},
|
||||
"protocol": {"PROTOCOL", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetProtocol()
|
||||
}},
|
||||
"user": {"USER", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return connection.GetUser()
|
||||
}},
|
||||
"process": {"PROCESS", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
processInfo := connection.GetProcessInfo()
|
||||
if processInfo.GetProcessPath() != "" {
|
||||
return processInfo.GetProcessPath()
|
||||
}
|
||||
if len(processInfo.GetPackageNames()) > 0 {
|
||||
return processInfo.GetPackageNames()[0]
|
||||
}
|
||||
return ""
|
||||
}},
|
||||
"created": {"CREATED", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return formatConnectionTime(connection.GetCreatedAt())
|
||||
}},
|
||||
"rate": {"RATE", func(connection *daemon.Connection, rates map[string]connectionRate) string {
|
||||
rate, found := rates[connection.GetId()]
|
||||
if !found || (rate.uplink == 0 && rate.downlink == 0) {
|
||||
return ""
|
||||
}
|
||||
return "↑" + byteformats.FormatBytes(uint64(rate.uplink)) + "/s ↓" + byteformats.FormatBytes(uint64(rate.downlink)) + "/s"
|
||||
}},
|
||||
"total": {"TOTAL", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
||||
return "↑" + byteformats.FormatBytes(uint64(connection.GetUplinkTotal())) + " ↓" + byteformats.FormatBytes(uint64(connection.GetDownlinkTotal()))
|
||||
}},
|
||||
}
|
||||
|
||||
var commandAPIConnectionList = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List open connections",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIConnectionList()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIConnectionList.Flags().StringSliceVar(
|
||||
&commandAPIConnectionListFlagColumns,
|
||||
"columns",
|
||||
[]string{"id", "network", "destination", "inbound", "outbound", "total"},
|
||||
"Columns to display (available: "+strings.Join(connectionColumnNames, ", ")+")",
|
||||
)
|
||||
commandAPIConnection.AddCommand(commandAPIConnectionList)
|
||||
}
|
||||
|
||||
func runAPIConnectionList() error {
|
||||
columns := make([]connectionColumn, 0, len(commandAPIConnectionListFlagColumns))
|
||||
sampleRates := false
|
||||
for _, name := range commandAPIConnectionListFlagColumns {
|
||||
column, found := connectionColumns[name]
|
||||
if !found {
|
||||
return E.New("unknown column: ", name, ", available: ", strings.Join(connectionColumnNames, ", "))
|
||||
}
|
||||
if name == "rate" {
|
||||
sampleRates = true
|
||||
}
|
||||
columns = append(columns, column)
|
||||
}
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
var (
|
||||
connections []*daemon.Connection
|
||||
rates map[string]connectionRate
|
||||
)
|
||||
if sampleRates {
|
||||
connections, rates, err = fetchConnectionsAndRates(client)
|
||||
} else {
|
||||
connections, err = fetchConnections(client)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
table := tableWriter{
|
||||
header: common.Map(columns, func(it connectionColumn) string {
|
||||
return it.header
|
||||
}),
|
||||
emptyMessage: "no connections",
|
||||
}
|
||||
for _, connection := range connections {
|
||||
if connection.GetClosedAt() != 0 {
|
||||
continue
|
||||
}
|
||||
table.addRow(common.Map(columns, func(it connectionColumn) string {
|
||||
return it.value(connection, rates)
|
||||
})...)
|
||||
}
|
||||
table.flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectionDestination(connection *daemon.Connection) string {
|
||||
destination := connection.GetDestination()
|
||||
domain := connection.GetDomain()
|
||||
if domain == "" {
|
||||
return destination
|
||||
}
|
||||
portIndex := strings.LastIndex(destination, ":")
|
||||
if portIndex == -1 {
|
||||
return domain
|
||||
}
|
||||
return domain + destination[portIndex:]
|
||||
}
|
||||
|
||||
func fetchConnectionsAndRates(client daemon.StartedServiceClient) ([]*daemon.Connection, map[string]connectionRate, error) {
|
||||
stream, err := client.SubscribeConnections(globalCtx, &daemon.SubscribeConnectionsRequest{Interval: int64(time.Second)})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
initialEvents, err := stream.Recv()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
connections := common.FilterNotNil(common.Map(initialEvents.GetEvents(), func(it *daemon.ConnectionEvent) *daemon.Connection {
|
||||
return it.GetConnection()
|
||||
}))
|
||||
common.SortBy(connections, func(it *daemon.Connection) int64 {
|
||||
return it.GetCreatedAt()
|
||||
})
|
||||
updateEvents, err := stream.Recv()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rates := make(map[string]connectionRate, len(updateEvents.GetEvents()))
|
||||
for _, event := range updateEvents.GetEvents() {
|
||||
rates[event.GetId()] = connectionRate{
|
||||
uplink: event.GetUplinkDelta(),
|
||||
downlink: event.GetDownlinkDelta(),
|
||||
}
|
||||
}
|
||||
return connections, rates, nil
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIConnectionShow = &cobra.Command{
|
||||
Use: "show <id>",
|
||||
Short: "Print connection details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIConnectionShow(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIConnection.AddCommand(commandAPIConnectionShow)
|
||||
}
|
||||
|
||||
func runAPIConnectionShow(connectionID string) error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
connections, err := fetchConnections(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connection := common.Find(connections, func(it *daemon.Connection) bool {
|
||||
return it.GetId() == connectionID
|
||||
})
|
||||
if connection == nil {
|
||||
return E.New("connection not found: ", connectionID)
|
||||
}
|
||||
state := "open"
|
||||
if connection.GetClosedAt() != 0 {
|
||||
state = "closed"
|
||||
}
|
||||
var ipVersion string
|
||||
if connection.GetIpVersion() != 0 {
|
||||
ipVersion = F.ToString(connection.GetIpVersion())
|
||||
}
|
||||
inbound := connection.GetInboundType()
|
||||
if connection.GetInbound() != "" {
|
||||
inbound = connection.GetInboundType() + "/" + connection.GetInbound()
|
||||
}
|
||||
outbound := connection.GetOutbound()
|
||||
if outbound != "" && connection.GetOutboundType() != "" {
|
||||
outbound = F.ToString(outbound, " (", connection.GetOutboundType(), ")")
|
||||
}
|
||||
var block blockWriter
|
||||
block.addLine("ID", connection.GetId())
|
||||
block.addLine("State", state)
|
||||
block.addLine("Created", formatConnectionTime(connection.GetCreatedAt()))
|
||||
block.addLine("Closed", formatConnectionTime(connection.GetClosedAt()))
|
||||
block.addLine("Network", connection.GetNetwork())
|
||||
block.addLine("IP version", ipVersion)
|
||||
block.addLine("Protocol", connection.GetProtocol())
|
||||
block.addLine("Inbound", inbound)
|
||||
block.addLine("Source", connection.GetSource())
|
||||
block.addLine("Destination", connection.GetDestination())
|
||||
block.addLine("Domain", connection.GetDomain())
|
||||
block.addLine("User", connection.GetUser())
|
||||
block.addLine("Process", formatProcessInfo(connection.GetProcessInfo()))
|
||||
block.addLine("Rule", connection.GetRule())
|
||||
block.addLine("Outbound", outbound)
|
||||
block.addLine("Chain", strings.Join(connection.GetChainList(), " <- "))
|
||||
block.addLine("From outbound", connection.GetFromOutbound())
|
||||
block.addLine("Uplink", byteformats.FormatBytes(uint64(connection.GetUplinkTotal())))
|
||||
block.addLine("Downlink", byteformats.FormatBytes(uint64(connection.GetDownlinkTotal())))
|
||||
block.flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatConnectionTime(timestamp int64) string {
|
||||
if timestamp == 0 {
|
||||
return ""
|
||||
}
|
||||
return time.UnixMilli(timestamp).Local().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func formatProcessInfo(processInfo *daemon.ProcessInfo) string {
|
||||
if processInfo == nil {
|
||||
return ""
|
||||
}
|
||||
var process string
|
||||
if processInfo.GetProcessPath() != "" {
|
||||
process = processInfo.GetProcessPath()
|
||||
} else if len(processInfo.GetPackageNames()) > 0 {
|
||||
process = processInfo.GetPackageNames()[0]
|
||||
}
|
||||
if process == "" {
|
||||
if processInfo.GetUserId() != -1 {
|
||||
process = F.ToString(processInfo.GetUserId())
|
||||
}
|
||||
} else if processInfo.GetUserName() != "" {
|
||||
process = F.ToString(process, " (", processInfo.GetUserName(), ")")
|
||||
} else if processInfo.GetUserId() != -1 {
|
||||
process = F.ToString(process, " (", processInfo.GetUserId(), ")")
|
||||
}
|
||||
return process
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var commandAPIGroup = &cobra.Command{
|
||||
Use: "group",
|
||||
Short: "Manage outbound groups",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIRoot.AddCommand(commandAPIGroup)
|
||||
}
|
||||
|
||||
func fetchGroups(client daemon.StartedServiceClient) ([]*daemon.Group, error) {
|
||||
stream, err := client.SubscribeGroups(globalCtx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups, err := stream.Recv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups.GetGroup(), nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIGroupList = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List outbound groups",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIGroupList()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIGroup.AddCommand(commandAPIGroupList)
|
||||
}
|
||||
|
||||
func runAPIGroupList() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
groups, err := fetchGroups(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
table := tableWriter{
|
||||
header: []string{"TAG", "TYPE", "SELECTED"},
|
||||
emptyMessage: "no groups",
|
||||
}
|
||||
for _, group := range groups {
|
||||
table.addRow(group.GetTag(), group.GetType(), group.GetSelected())
|
||||
}
|
||||
table.flush()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIGroupSelect = &cobra.Command{
|
||||
Use: "select <group> <outbound>",
|
||||
Short: "Select an outbound in a group",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIGroupSelect(args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIGroup.AddCommand(commandAPIGroupSelect)
|
||||
}
|
||||
|
||||
func runAPIGroupSelect(groupTag string, outboundTag string) error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
_, err = client.SelectOutbound(globalCtx, &daemon.SelectOutboundRequest{
|
||||
GroupTag: groupTag,
|
||||
OutboundTag: outboundTag,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIGroupShow = &cobra.Command{
|
||||
Use: "show <group>",
|
||||
Short: "Show an outbound group",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIGroupShow(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIGroup.AddCommand(commandAPIGroupShow)
|
||||
}
|
||||
|
||||
func runAPIGroupShow(groupTag string) error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
groups, err := fetchGroups(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, group := range groups {
|
||||
if group.GetTag() != groupTag {
|
||||
continue
|
||||
}
|
||||
block := blockWriter{}
|
||||
block.addLine("Tag", group.GetTag())
|
||||
block.addLine("Type", group.GetType())
|
||||
block.addLine("Selected", group.GetSelected())
|
||||
block.flush()
|
||||
table := tableWriter{
|
||||
header: []string{"TAG", "TYPE", "DELAY"},
|
||||
}
|
||||
for _, item := range group.GetItems() {
|
||||
table.addRow(item.GetTag(), item.GetType(), formatDelay(item.GetUrlTestDelay()))
|
||||
}
|
||||
table.flush()
|
||||
return nil
|
||||
}
|
||||
return E.New("group not found: ", groupTag)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIGroupURLTest = &cobra.Command{
|
||||
Use: "urltest <group>",
|
||||
Short: "Start a URL test",
|
||||
Long: "Start a URL test.\n\nThe tests are only spawned: results appear in `outbounds --group <group>` a few seconds later.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIGroupURLTest(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIGroup.AddCommand(commandAPIGroupURLTest)
|
||||
}
|
||||
|
||||
func runAPIGroupURLTest(groupTag string) error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
_, err = client.URLTest(globalCtx, &daemon.URLTestRequest{OutboundTag: groupTag})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAPILogsFlagFollow bool
|
||||
commandAPILogsFlagLevel string
|
||||
commandAPILogsFlagSearch string
|
||||
)
|
||||
|
||||
var commandAPILogs = &cobra.Command{
|
||||
Use: "logs",
|
||||
Short: "Print the service logs",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPILogs()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPILogs.Flags().BoolVarP(&commandAPILogsFlagFollow, "follow", "f", false, "Keep printing new log entries until interrupted")
|
||||
commandAPILogs.Flags().StringVar(&commandAPILogsFlagLevel, "level", "", "Print entries at this level or more severe (default: the service log level)")
|
||||
commandAPILogs.Flags().StringVar(&commandAPILogsFlagSearch, "search", "", "Print entries containing this text, case-insensitive")
|
||||
commandAPIRoot.AddCommand(commandAPILogs)
|
||||
}
|
||||
|
||||
func runAPILogs() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
var level log.Level
|
||||
if commandAPILogsFlagLevel != "" {
|
||||
level, err = log.ParseLevel(commandAPILogsFlagLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
defaultLevel, levelErr := client.GetDefaultLogLevel(ctx, &emptypb.Empty{})
|
||||
if levelErr != nil {
|
||||
return levelErr
|
||||
}
|
||||
level = log.Level(defaultLevel.GetLevel())
|
||||
}
|
||||
stream, err := client.SubscribeLog(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
searchQuery := strings.ToLower(strings.TrimSpace(commandAPILogsFlagSearch))
|
||||
for backlog := true; ; backlog = false {
|
||||
message, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return recvErr
|
||||
}
|
||||
if message.GetReset_() && len(message.GetMessages()) == 0 && !backlog {
|
||||
writeStderrLine("log buffer cleared")
|
||||
continue
|
||||
}
|
||||
var output strings.Builder
|
||||
for _, entry := range message.GetMessages() {
|
||||
if log.Level(entry.GetLevel()) > level {
|
||||
continue
|
||||
}
|
||||
plainMessage := stripColors(entry.GetMessage())
|
||||
if searchQuery != "" && !strings.Contains(strings.ToLower(plainMessage), searchQuery) {
|
||||
continue
|
||||
}
|
||||
if stdoutIsTerminal {
|
||||
output.WriteString(entry.GetMessage())
|
||||
} else {
|
||||
output.WriteString(plainMessage)
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
os.Stdout.WriteString(output.String())
|
||||
if backlog && !commandAPILogsFlagFollow {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var commandAPIMode = &cobra.Command{
|
||||
Use: "mode",
|
||||
Short: "Print the current clash mode",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIMode()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIRoot.AddCommand(commandAPIMode)
|
||||
}
|
||||
|
||||
func runAPIMode() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
modeStatus, err := client.GetClashModeStatus(globalCtx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentMode := modeStatus.GetCurrentMode()
|
||||
if currentMode == "" {
|
||||
currentMode = "-"
|
||||
}
|
||||
os.Stdout.WriteString(currentMode + "\n")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var commandAPIModeList = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List clash modes",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIModeList()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIMode.AddCommand(commandAPIModeList)
|
||||
}
|
||||
|
||||
func runAPIModeList() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
modeStatus, err := client.GetClashModeStatus(globalCtx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(modeStatus.GetModeList()) == 0 {
|
||||
writeStderrLine("no clash modes")
|
||||
return nil
|
||||
}
|
||||
var output strings.Builder
|
||||
for _, mode := range modeStatus.GetModeList() {
|
||||
output.WriteString(mode)
|
||||
output.WriteString("\n")
|
||||
}
|
||||
os.Stdout.WriteString(output.String())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIModeSet = &cobra.Command{
|
||||
Use: "set <mode>",
|
||||
Short: "Set the clash mode",
|
||||
Long: "Set the clash mode.\n\nThe value is not validated against the mode list: setting an unknown mode reports success.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIModeSet(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIMode.AddCommand(commandAPIModeSet)
|
||||
}
|
||||
|
||||
func runAPIModeSet(mode string) error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
_, err = client.SetClashMode(globalCtx, &daemon.ClashMode{Mode: mode})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAPINetworkQualityFlagConfigURL string
|
||||
commandAPINetworkQualityFlagSerial bool
|
||||
commandAPINetworkQualityFlagMaxRuntime int
|
||||
commandAPINetworkQualityFlagHTTP3 bool
|
||||
commandAPINetworkQualityFlagOutbound string
|
||||
)
|
||||
|
||||
var commandAPINetworkQuality = &cobra.Command{
|
||||
Use: "networkquality",
|
||||
Short: "Run a network quality test",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPINetworkQuality()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPINetworkQuality.Flags().StringVar(
|
||||
&commandAPINetworkQualityFlagConfigURL,
|
||||
"config-url", "",
|
||||
"Network quality test config URL (default: Apple mensura)",
|
||||
)
|
||||
commandAPINetworkQuality.Flags().BoolVar(
|
||||
&commandAPINetworkQualityFlagSerial,
|
||||
"serial", false,
|
||||
"Run download and upload tests sequentially instead of in parallel",
|
||||
)
|
||||
commandAPINetworkQuality.Flags().IntVar(
|
||||
&commandAPINetworkQualityFlagMaxRuntime,
|
||||
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
|
||||
"Network quality maximum runtime in seconds",
|
||||
)
|
||||
commandAPINetworkQuality.Flags().BoolVar(
|
||||
&commandAPINetworkQualityFlagHTTP3,
|
||||
"http3", false,
|
||||
"Use HTTP/3 (QUIC) for measurement traffic",
|
||||
)
|
||||
commandAPINetworkQuality.Flags().StringVarP(
|
||||
&commandAPINetworkQualityFlagOutbound,
|
||||
"outbound", "o", "",
|
||||
"Use specified tag instead of default outbound",
|
||||
)
|
||||
commandAPIRoot.AddCommand(commandAPINetworkQuality)
|
||||
}
|
||||
|
||||
func runAPINetworkQuality() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
stream, err := client.StartNetworkQualityTest(globalCtx, &daemon.NetworkQualityTestRequest{
|
||||
ConfigURL: commandAPINetworkQualityFlagConfigURL,
|
||||
OutboundTag: commandAPINetworkQualityFlagOutbound,
|
||||
Serial: commandAPINetworkQualityFlagSerial,
|
||||
MaxRuntimeSeconds: int32(commandAPINetworkQualityFlagMaxRuntime),
|
||||
Http3: commandAPINetworkQualityFlagHTTP3,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeStderrLine("==== NETWORK QUALITY TEST ====")
|
||||
for {
|
||||
progress, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
return recvErr
|
||||
}
|
||||
if !progress.GetIsFinal() {
|
||||
writeNetworkQualityProgress(progress)
|
||||
continue
|
||||
}
|
||||
writeStderrLine("")
|
||||
if progress.GetError() != "" {
|
||||
return E.New(progress.GetError())
|
||||
}
|
||||
writeStderrLine(strings.Repeat("-", 40))
|
||||
fmt.Fprintf(os.Stdout, "Idle Latency: %d ms\n", progress.GetIdleLatencyMs())
|
||||
fmt.Fprintf(os.Stdout, "Download Capacity: %-20s Accuracy: %s\n",
|
||||
networkquality.FormatBitrate(progress.GetDownloadCapacity()),
|
||||
networkquality.Accuracy(progress.GetDownloadCapacityAccuracy()))
|
||||
fmt.Fprintf(os.Stdout, "Upload Capacity: %-20s Accuracy: %s\n",
|
||||
networkquality.FormatBitrate(progress.GetUploadCapacity()),
|
||||
networkquality.Accuracy(progress.GetUploadCapacityAccuracy()))
|
||||
fmt.Fprintf(os.Stdout, "Download Responsiveness: %-20s Accuracy: %s\n",
|
||||
fmt.Sprintf("%d RPM", progress.GetDownloadRPM()),
|
||||
networkquality.Accuracy(progress.GetDownloadRPMAccuracy()))
|
||||
fmt.Fprintf(os.Stdout, "Upload Responsiveness: %-20s Accuracy: %s\n",
|
||||
fmt.Sprintf("%d RPM", progress.GetUploadRPM()),
|
||||
networkquality.Accuracy(progress.GetUploadRPMAccuracy()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func writeNetworkQualityProgress(progress *daemon.NetworkQualityTestProgress) {
|
||||
if !commandAPINetworkQualityFlagSerial && networkquality.Phase(progress.GetPhase()) != networkquality.PhaseIdle {
|
||||
writeProgress(fmt.Sprintf("Download: %s RPM: %d Upload: %s RPM: %d",
|
||||
networkquality.FormatBitrate(progress.GetDownloadCapacity()), progress.GetDownloadRPM(),
|
||||
networkquality.FormatBitrate(progress.GetUploadCapacity()), progress.GetUploadRPM()))
|
||||
return
|
||||
}
|
||||
switch networkquality.Phase(progress.GetPhase()) {
|
||||
case networkquality.PhaseIdle:
|
||||
if progress.GetIdleLatencyMs() > 0 {
|
||||
writeProgress(fmt.Sprintf("Idle Latency: %d ms", progress.GetIdleLatencyMs()))
|
||||
} else {
|
||||
writeProgress("Measuring idle latency...")
|
||||
}
|
||||
case networkquality.PhaseDownload:
|
||||
writeProgress(fmt.Sprintf("Download: %s RPM: %d",
|
||||
networkquality.FormatBitrate(progress.GetDownloadCapacity()), progress.GetDownloadRPM()))
|
||||
case networkquality.PhaseUpload:
|
||||
writeProgress(fmt.Sprintf("Upload: %s RPM: %d",
|
||||
networkquality.FormatBitrate(progress.GetUploadCapacity()), progress.GetUploadRPM()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var commandAPIOpenConnect = &cobra.Command{
|
||||
Use: "openconnect",
|
||||
Short: "Manage OpenConnect authentication",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIRoot.AddCommand(commandAPIOpenConnect)
|
||||
}
|
||||
|
||||
func subscribeOpenConnectStatus(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.OpenConnectStatusUpdate], []*daemon.OpenConnectEndpointStatus, error) {
|
||||
stream, err := client.SubscribeOpenConnectStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
endpoints, err := recvOpenConnectStatus(stream)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return stream, endpoints, nil
|
||||
}
|
||||
|
||||
func recvOpenConnectStatus(stream grpc.ServerStreamingClient[daemon.OpenConnectStatusUpdate]) ([]*daemon.OpenConnectEndpointStatus, error) {
|
||||
update, err := stream.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil, E.New("api service closed the status stream")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return update.GetEndpoints(), nil
|
||||
}
|
||||
|
||||
func openConnectChallengeSummary(challenge *daemon.OpenConnectAuthChallenge) string {
|
||||
form := challenge.GetForm()
|
||||
if form != nil {
|
||||
return F.ToString("form (", len(form.GetFields()), " fields)")
|
||||
}
|
||||
browser := challenge.GetBrowser()
|
||||
if browser == nil {
|
||||
return "unknown"
|
||||
}
|
||||
mode, err := deriveOpenConnectBrowserMode(browser)
|
||||
if err != nil {
|
||||
return "browser (invalid)"
|
||||
}
|
||||
return "browser (" + mode + ")"
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAPIOpenConnectAuthFlagEndpoint string
|
||||
commandAPIOpenConnectAuthFlagCallbackPort uint16
|
||||
)
|
||||
|
||||
var commandAPIOpenConnectAuth = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Answer OpenConnect authentication challenges",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := runAPIOpenConnectAuth()
|
||||
if errors.Is(err, errAuthInterrupted) {
|
||||
writeAuthLine(`interrupted; the challenge is still pending — run "sing-box api openconnect auth" again, or "sing-box api openconnect cancel" to restart authentication`)
|
||||
os.Exit(130)
|
||||
}
|
||||
return wrapAuthError("openconnect", err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenConnectAuth.Flags().StringVar(&commandAPIOpenConnectAuthFlagEndpoint, "endpoint", "", "OpenConnect endpoint tag (default: the only configured endpoint)")
|
||||
commandAPIOpenConnectAuth.Flags().Uint16Var(&commandAPIOpenConnectAuthFlagCallbackPort, "callback-port", 8020, "Local port for the browser single sign-on callback listener")
|
||||
commandAPIOpenConnect.AddCommand(commandAPIOpenConnectAuth)
|
||||
}
|
||||
|
||||
func runAPIOpenConnectAuth() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
stream, endpoints, err := subscribeOpenConnectStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointStatus, err := resolveVPNEndpoint(endpoints, commandAPIOpenConnectAuthFlagEndpoint, "openconnect")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointTag := endpointStatus.GetEndpointTag()
|
||||
if endpointStatus.GetAuthChallenge() == nil {
|
||||
switch endpointStatus.GetState() {
|
||||
case adapter.OpenConnectStateConnected:
|
||||
return E.New("endpoint ", endpointTag, " is already connected")
|
||||
case adapter.OpenConnectStateError:
|
||||
return E.New("endpoint ", endpointTag, " failed: ", endpointStatus.GetError())
|
||||
}
|
||||
}
|
||||
watcher := newVPNStatusWatcher(endpoints, func() ([]*daemon.OpenConnectEndpointStatus, error) {
|
||||
return recvOpenConnectStatus(stream)
|
||||
})
|
||||
err = openConnectAuthLoop(ctx, client, watcher, newInteractiveInput(), endpointTag)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return errAuthInterrupted
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func openConnectAuthLoop(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenConnectEndpointStatus],
|
||||
input *interactiveInput,
|
||||
endpointTag string,
|
||||
) error {
|
||||
var (
|
||||
renderedID string
|
||||
waitingPrinted bool
|
||||
)
|
||||
for {
|
||||
endpoints, updated, streamErr := watcher.current()
|
||||
if streamErr != nil {
|
||||
return streamErr
|
||||
}
|
||||
index := slices.IndexFunc(endpoints, func(it *daemon.OpenConnectEndpointStatus) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 {
|
||||
return E.New("endpoint not found: ", endpointTag)
|
||||
}
|
||||
endpointStatus := endpoints[index]
|
||||
challenge := endpointStatus.GetAuthChallenge()
|
||||
switch {
|
||||
case challenge == nil && endpointStatus.GetState() == adapter.OpenConnectStateConnected:
|
||||
os.Stdout.WriteString(endpointTag + ": connected\n")
|
||||
return nil
|
||||
case challenge == nil && endpointStatus.GetState() == adapter.OpenConnectStateError:
|
||||
return E.New("endpoint ", endpointTag, " failed: ", endpointStatus.GetError())
|
||||
case challenge != nil && challenge.GetId() != renderedID:
|
||||
renderedID = challenge.GetId()
|
||||
waitingPrinted = false
|
||||
handleErr := handleOpenConnectChallenge(ctx, client, watcher, input, endpointTag, challenge)
|
||||
switch {
|
||||
case handleErr == nil:
|
||||
case errors.Is(handleErr, errAuthChallengeWithdrawn):
|
||||
writeAuthLine(errAuthChallengeWithdrawn.Error())
|
||||
default:
|
||||
return handleErr
|
||||
}
|
||||
continue
|
||||
case challenge == nil && !waitingPrinted:
|
||||
waitingPrinted = true
|
||||
writeAuthLine("waiting for an authentication challenge on " + endpointTag + "...")
|
||||
}
|
||||
select {
|
||||
case <-updated:
|
||||
case <-ctx.Done():
|
||||
return errAuthInterrupted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleOpenConnectChallenge(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenConnectEndpointStatus],
|
||||
input *interactiveInput,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenConnectAuthChallenge,
|
||||
) error {
|
||||
prompter := &authPrompter{ctx: ctx, input: input, aborted: make(chan struct{})}
|
||||
watchCtx, cancelWatch := context.WithCancel(ctx)
|
||||
defer cancelWatch()
|
||||
go watchOpenConnectChallenge(watchCtx, watcher, endpointTag, challenge.GetId(), prompter)
|
||||
form := challenge.GetForm()
|
||||
browser := challenge.GetBrowser()
|
||||
switch {
|
||||
case form != nil:
|
||||
return submitOpenConnectForm(ctx, client, prompter, endpointTag, challenge, form)
|
||||
case browser != nil:
|
||||
return submitOpenConnectBrowser(ctx, client, prompter, endpointTag, challenge, browser)
|
||||
default:
|
||||
return E.New("unsupported authentication challenge")
|
||||
}
|
||||
}
|
||||
|
||||
func watchOpenConnectChallenge(
|
||||
ctx context.Context,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenConnectEndpointStatus],
|
||||
endpointTag string,
|
||||
challengeID string,
|
||||
prompter *authPrompter,
|
||||
) {
|
||||
for {
|
||||
endpoints, updated, streamErr := watcher.current()
|
||||
if streamErr != nil {
|
||||
prompter.abort(streamErr)
|
||||
return
|
||||
}
|
||||
index := slices.IndexFunc(endpoints, func(it *daemon.OpenConnectEndpointStatus) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 || endpoints[index].GetAuthChallenge().GetId() != challengeID {
|
||||
prompter.abort(errAuthChallengeWithdrawn)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-updated:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenConnectForm(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
prompter *authPrompter,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenConnectAuthChallenge,
|
||||
form *daemon.OpenConnectAuthForm,
|
||||
) error {
|
||||
if !authInputIsTerminal {
|
||||
return errAuthNotInteractive
|
||||
}
|
||||
writeAuthHeader(endpointTag, "authentication")
|
||||
preambleWritten := false
|
||||
if challenge.GetBanner() != "" {
|
||||
writeAuthBanner(challenge.GetBanner())
|
||||
preambleWritten = true
|
||||
}
|
||||
if challenge.GetError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetError())
|
||||
preambleWritten = true
|
||||
}
|
||||
if challenge.GetMessage() != "" {
|
||||
writeAuthLine(challenge.GetMessage())
|
||||
preambleWritten = true
|
||||
}
|
||||
if preambleWritten {
|
||||
writeAuthLine("")
|
||||
}
|
||||
for {
|
||||
values := make(map[string]string, len(form.GetFields()))
|
||||
for _, field := range form.GetFields() {
|
||||
value, err := promptOpenConnectField(prompter, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values[field.GetSubmissionKey()] = value
|
||||
}
|
||||
_, err := client.SubmitOpenConnectAuthResponse(ctx, &daemon.OpenConnectAuthResponseSubmission{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
Response: &daemon.OpenConnectAuthResponseSubmission_Form{
|
||||
Form: &daemon.OpenConnectAuthFormResponse{Values: values},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
outcome, message := classifySubmitError(err)
|
||||
switch outcome {
|
||||
case submitStale:
|
||||
return errAuthChallengeWithdrawn
|
||||
case submitFatal:
|
||||
return err
|
||||
}
|
||||
writeAuthError("openconnect", "submit rejected: "+message)
|
||||
}
|
||||
}
|
||||
|
||||
func promptOpenConnectField(prompter *authPrompter, field *daemon.OpenConnectAuthFormField) (string, error) {
|
||||
label := field.GetLabel()
|
||||
if label == "" {
|
||||
label = field.GetName()
|
||||
}
|
||||
switch field.GetKind() {
|
||||
case "text":
|
||||
return prompter.promptText(label, field.GetValue())
|
||||
case "password":
|
||||
return prompter.promptPassword(label, field.GetValue())
|
||||
case "select":
|
||||
return promptOpenConnectSelect(prompter, label, field.GetOptions(), field.GetValue())
|
||||
default:
|
||||
return "", E.New("unsupported authentication field kind: ", field.GetKind())
|
||||
}
|
||||
}
|
||||
|
||||
func promptOpenConnectSelect(prompter *authPrompter, label string, options []*daemon.OpenConnectAuthFormChoice, defaultValue string) (string, error) {
|
||||
prompt := strings.TrimSuffix(label, ":")
|
||||
var menu strings.Builder
|
||||
menu.WriteString(prompt + ":\n")
|
||||
for index, option := range options {
|
||||
optionLabel := option.GetLabel()
|
||||
if optionLabel == "" {
|
||||
optionLabel = option.GetValue()
|
||||
}
|
||||
menu.WriteString(" " + strconv.Itoa(index+1) + ") " + optionLabel)
|
||||
if option.GetValue() == defaultValue {
|
||||
menu.WriteString(" [default]")
|
||||
}
|
||||
menu.WriteString("\n")
|
||||
}
|
||||
os.Stderr.WriteString(menu.String())
|
||||
for {
|
||||
line, err := prompter.read(prompt+": ", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" && defaultValue != "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
selected, parseErr := strconv.Atoi(line)
|
||||
if parseErr == nil && selected >= 1 && selected <= len(options) {
|
||||
return options[selected-1].GetValue(), nil
|
||||
}
|
||||
if slices.ContainsFunc(options, func(it *daemon.OpenConnectAuthFormChoice) bool {
|
||||
return it.GetValue() == line
|
||||
}) {
|
||||
return line, nil
|
||||
}
|
||||
writeAuthLine("select a number between 1 and " + strconv.Itoa(len(options)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
const (
|
||||
openConnectBrowserModeCallback = "callback"
|
||||
openConnectBrowserModeCookies = "cookies"
|
||||
openConnectBrowserModeHeaders = "headers"
|
||||
)
|
||||
|
||||
const openConnectCallbackPage = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>single sign-on</title></head>
|
||||
<body style="font-family:system-ui,sans-serif;text-align:center;margin-top:4em">
|
||||
<h3>Single sign-on completed</h3>
|
||||
<p>You may close this tab and return to the terminal.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func deriveOpenConnectBrowserMode(request *daemon.OpenConnectBrowserRequest) (string, error) {
|
||||
callbackMode := len(request.GetCallbackURLPrefixes()) > 0
|
||||
cookieMode := request.GetFinalURL() != "" || len(request.GetCookieNames()) > 0 || len(request.GetEarlyCookieNames()) > 0
|
||||
headerMode := len(request.GetHeaderNames()) > 0
|
||||
selectedModes := common.Filter([]bool{callbackMode, cookieMode, headerMode}, func(it bool) bool {
|
||||
return it
|
||||
})
|
||||
invalidRequest := E.New("openconnect browser request must select exactly one completion mode")
|
||||
if len(selectedModes) != 1 {
|
||||
return "", invalidRequest
|
||||
}
|
||||
switch {
|
||||
case callbackMode:
|
||||
if len(common.Uniq(request.GetCallbackURLPrefixes())) != len(request.GetCallbackURLPrefixes()) {
|
||||
return "", invalidRequest
|
||||
}
|
||||
return openConnectBrowserModeCallback, nil
|
||||
case cookieMode:
|
||||
cookieNames := append(slices.Clone(request.GetCookieNames()), request.GetEarlyCookieNames()...)
|
||||
if len(common.Uniq(cookieNames)) != len(cookieNames) {
|
||||
return "", invalidRequest
|
||||
}
|
||||
return openConnectBrowserModeCookies, nil
|
||||
default:
|
||||
headerNames := common.Map(request.GetHeaderNames(), strings.ToLower)
|
||||
if len(common.Uniq(headerNames)) != len(headerNames) {
|
||||
return "", invalidRequest
|
||||
}
|
||||
return openConnectBrowserModeHeaders, nil
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenConnectBrowser(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
prompter *authPrompter,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenConnectAuthChallenge,
|
||||
request *daemon.OpenConnectBrowserRequest,
|
||||
) error {
|
||||
mode, err := deriveOpenConnectBrowserMode(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeAuthHeader(endpointTag, "browser authentication")
|
||||
if challenge.GetError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetError())
|
||||
}
|
||||
if challenge.GetMessage() != "" {
|
||||
writeAuthLine(challenge.GetMessage())
|
||||
}
|
||||
for {
|
||||
result, earlyFailure, collectErr := collectOpenConnectBrowserResult(ctx, prompter, mode, request)
|
||||
if collectErr != nil {
|
||||
return collectErr
|
||||
}
|
||||
warnPlaintextAPIConnection()
|
||||
_, submitErr := client.SubmitOpenConnectAuthResponse(ctx, &daemon.OpenConnectAuthResponseSubmission{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
Response: &daemon.OpenConnectAuthResponseSubmission_Browser{Browser: result},
|
||||
})
|
||||
if submitErr == nil {
|
||||
if earlyFailure {
|
||||
writeAuthLine("single sign-on failed; the client will retry authentication")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
outcome, message := classifySubmitError(submitErr)
|
||||
switch outcome {
|
||||
case submitStale:
|
||||
return errAuthChallengeWithdrawn
|
||||
case submitFatal:
|
||||
return submitErr
|
||||
}
|
||||
writeAuthError("openconnect", "browser authentication rejected: "+message)
|
||||
}
|
||||
}
|
||||
|
||||
func collectOpenConnectBrowserResult(
|
||||
ctx context.Context,
|
||||
prompter *authPrompter,
|
||||
mode string,
|
||||
request *daemon.OpenConnectBrowserRequest,
|
||||
) (*daemon.OpenConnectBrowserResult, bool, error) {
|
||||
switch {
|
||||
case mode == openConnectBrowserModeCallback:
|
||||
target, err := parseOpenConnectCallbackTarget(request.GetCallbackURLPrefixes())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !authInputIsTerminal {
|
||||
return nil, false, errAuthNotInteractive
|
||||
}
|
||||
finalURL, err := runOpenConnectCallbackListener(ctx, prompter, target, request.GetUrl())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &daemon.OpenConnectBrowserResult{FinalURL: finalURL}, false, nil
|
||||
case mode == openConnectBrowserModeCookies && len(request.GetCookieNames()) > 0:
|
||||
if !authInputIsTerminal {
|
||||
return nil, false, errAuthNotInteractive
|
||||
}
|
||||
return promptOpenConnectBrowserCookies(prompter, request)
|
||||
case mode == openConnectBrowserModeHeaders:
|
||||
return nil, false, E.New("this single sign-on requires reading HTTP response headers, which cannot be done manually; use the sing-box desktop application")
|
||||
default:
|
||||
return nil, false, E.New("this single sign-on cannot be completed manually; use the sing-box desktop application")
|
||||
}
|
||||
}
|
||||
|
||||
type openConnectCallbackTarget struct {
|
||||
scheme string
|
||||
host string
|
||||
port string
|
||||
}
|
||||
|
||||
func (t openConnectCallbackTarget) resolve(requestURI string) string {
|
||||
return t.scheme + "://" + net.JoinHostPort(t.host, t.port) + requestURI
|
||||
}
|
||||
|
||||
func parseOpenConnectCallbackTarget(prefixes []string) (openConnectCallbackTarget, error) {
|
||||
var target openConnectCallbackTarget
|
||||
for _, prefix := range prefixes {
|
||||
parsed, err := url.Parse(prefix)
|
||||
if err != nil || !isLoopbackHost(parsed.Hostname()) {
|
||||
return target, E.New("callback URL prefix is not on loopback: ", prefix)
|
||||
}
|
||||
if target.scheme == "" {
|
||||
target.scheme = parsed.Scheme
|
||||
target.host = parsed.Hostname()
|
||||
}
|
||||
if target.port == "" {
|
||||
target.port = parsed.Port()
|
||||
}
|
||||
}
|
||||
if target.port == "" {
|
||||
target.port = strconv.Itoa(int(commandAPIOpenConnectAuthFlagCallbackPort))
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func runOpenConnectCallbackListener(ctx context.Context, prompter *authPrompter, target openConnectCallbackTarget, loginURL string) (string, error) {
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", target.port))
|
||||
if err != nil {
|
||||
return "", E.New("cannot listen on 127.0.0.1:", target.port, ": ", err.Error(), "; pass --callback-port")
|
||||
}
|
||||
defer listener.Close()
|
||||
writeAuthLine("Complete single sign-on in your browser; this command finishes automatically.")
|
||||
writeAuthLine("")
|
||||
writeAuthLine(" listening on " + target.resolve("/"))
|
||||
writeAuthLine(" url " + loginURL)
|
||||
writeAuthLine("")
|
||||
confirmed, err := prompter.promptConfirm("Open it now? [Y/n] ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !confirmed {
|
||||
writeAuthLine("waiting for the callback...")
|
||||
} else {
|
||||
openErr := openURLInBrowser(loginURL)
|
||||
if openErr != nil {
|
||||
writeAuthLine("failed to open the default browser: " + openErr.Error())
|
||||
writeAuthLine("waiting for the callback...")
|
||||
} else {
|
||||
writeAuthLine("opened in the default browser; waiting for the callback...")
|
||||
}
|
||||
}
|
||||
requestURIs := make(chan string, 1)
|
||||
server := &http.Server{Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
select {
|
||||
case requestURIs <- request.RequestURI:
|
||||
default:
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.Header().Set("Connection", "close")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.Write([]byte(openConnectCallbackPage))
|
||||
})}
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
defer server.Close()
|
||||
select {
|
||||
case requestURI := <-requestURIs:
|
||||
writeAuthLine("received callback")
|
||||
return target.resolve(requestURI), nil
|
||||
case <-prompter.aborted:
|
||||
return "", prompter.abortErr
|
||||
case <-ctx.Done():
|
||||
return "", errAuthInterrupted
|
||||
}
|
||||
}
|
||||
|
||||
func promptOpenConnectBrowserCookies(prompter *authPrompter, request *daemon.OpenConnectBrowserRequest) (*daemon.OpenConnectBrowserResult, bool, error) {
|
||||
writeAuthLine("This single sign-on must be completed manually.")
|
||||
writeAuthLine("")
|
||||
step := 1
|
||||
writeAuthLine(" " + strconv.Itoa(step) + ". Open this URL in any browser:")
|
||||
writeAuthLine(" " + request.GetUrl())
|
||||
if request.GetFinalURL() != "" {
|
||||
step++
|
||||
writeAuthLine(" " + strconv.Itoa(step) + ". Sign in until the browser lands on:")
|
||||
writeAuthLine(" " + request.GetFinalURL())
|
||||
}
|
||||
step++
|
||||
writeAuthLine(" " + strconv.Itoa(step) + ". Open the developer tools (F12) > Application > Cookies, and read the")
|
||||
writeAuthLine(" value of the cookie listed below for that page.")
|
||||
writeAuthLine("")
|
||||
earlyCookieNames := request.GetEarlyCookieNames()
|
||||
var cookies []*daemon.OpenConnectBrowserCookie
|
||||
for index, name := range request.GetCookieNames() {
|
||||
prompt := `Cookie "` + name + `": `
|
||||
if index == 0 && len(earlyCookieNames) > 0 {
|
||||
prompt = `Cookie "` + name + `" (or "!" if the page reported an error): `
|
||||
}
|
||||
for {
|
||||
value, err := prompter.read(prompt, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if index == 0 && len(earlyCookieNames) > 0 && value == "!" {
|
||||
earlyCookie, earlyErr := promptOpenConnectEarlyCookie(prompter, earlyCookieNames[0])
|
||||
if earlyErr != nil {
|
||||
return nil, false, earlyErr
|
||||
}
|
||||
return &daemon.OpenConnectBrowserResult{Cookies: []*daemon.OpenConnectBrowserCookie{earlyCookie}}, true, nil
|
||||
}
|
||||
if value == "" {
|
||||
writeAuthLine("cookie value must not be empty")
|
||||
continue
|
||||
}
|
||||
cookies = append(cookies, &daemon.OpenConnectBrowserCookie{Name: name, Value: value})
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(cookies) == 1 {
|
||||
writeAuthLine("submitting 1 cookie")
|
||||
} else {
|
||||
writeAuthLine(F.ToString("submitting ", len(cookies), " cookies"))
|
||||
}
|
||||
return &daemon.OpenConnectBrowserResult{FinalURL: request.GetFinalURL(), Cookies: cookies}, false, nil
|
||||
}
|
||||
|
||||
func promptOpenConnectEarlyCookie(prompter *authPrompter, name string) (*daemon.OpenConnectBrowserCookie, error) {
|
||||
for {
|
||||
value, err := prompter.read(`Error cookie "`+name+`": `, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "" {
|
||||
writeAuthLine("cookie value must not be empty")
|
||||
continue
|
||||
}
|
||||
return &daemon.OpenConnectBrowserCookie{Name: name, Value: value}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIOpenConnectCancelFlagEndpoint string
|
||||
|
||||
var commandAPIOpenConnectCancel = &cobra.Command{
|
||||
Use: "cancel",
|
||||
Short: "Cancel the pending OpenConnect authentication challenge",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIOpenConnectCancel()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenConnectCancel.Flags().StringVar(&commandAPIOpenConnectCancelFlagEndpoint, "endpoint", "", "OpenConnect endpoint tag (default: the only configured endpoint)")
|
||||
commandAPIOpenConnect.AddCommand(commandAPIOpenConnectCancel)
|
||||
}
|
||||
|
||||
func runAPIOpenConnectCancel() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
defer cancel()
|
||||
_, endpoints, err := subscribeOpenConnectStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointStatus, err := resolveVPNEndpoint(endpoints, commandAPIOpenConnectCancelFlagEndpoint, "openconnect")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointTag := endpointStatus.GetEndpointTag()
|
||||
challenge := endpointStatus.GetAuthChallenge()
|
||||
if challenge == nil {
|
||||
return E.New("no pending authentication challenge on ", endpointTag)
|
||||
}
|
||||
_, err = client.CancelOpenConnectAuthChallenge(ctx, &daemon.OpenConnectAuthChallengeCancel{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString(endpointTag + ": authentication challenge canceled; the client will restart authentication\n")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
errAuthInterrupted = E.New("interrupted")
|
||||
errAuthChallengeWithdrawn = E.New("challenge no longer pending, waiting for the next one")
|
||||
errAuthDeadlineExpired = E.New("challenge deadline expired; the server will retry the connection")
|
||||
errAuthNotInteractive = E.New("authentication requires an interactive terminal")
|
||||
)
|
||||
|
||||
var authInputIsTerminal = term.IsTerminal(int(os.Stdin.Fd())) && stderrIsTerminal
|
||||
|
||||
type vpnEndpointStatus interface {
|
||||
GetEndpointTag() string
|
||||
}
|
||||
|
||||
func resolveVPNEndpoint[T vpnEndpointStatus](endpoints []T, endpointTag string, domain string) (T, error) {
|
||||
var zero T
|
||||
if endpointTag != "" {
|
||||
index := slices.IndexFunc(endpoints, func(it T) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 {
|
||||
return zero, E.New("endpoint not found: ", endpointTag)
|
||||
}
|
||||
return endpoints[index], nil
|
||||
}
|
||||
switch len(endpoints) {
|
||||
case 0:
|
||||
return zero, E.New("no ", domain, " endpoint is configured")
|
||||
case 1:
|
||||
return endpoints[0], nil
|
||||
default:
|
||||
return zero, E.New("multiple ", domain, " endpoints; select one with -e: ", strings.Join(common.Map(endpoints, func(it T) string {
|
||||
return it.GetEndpointTag()
|
||||
}), ", "))
|
||||
}
|
||||
}
|
||||
|
||||
type vpnStatusWatcher[T any] struct {
|
||||
access sync.Mutex
|
||||
updated chan struct{}
|
||||
endpoints []T
|
||||
err error
|
||||
}
|
||||
|
||||
func newVPNStatusWatcher[T any](endpoints []T, recv func() ([]T, error)) *vpnStatusWatcher[T] {
|
||||
watcher := &vpnStatusWatcher[T]{
|
||||
updated: make(chan struct{}),
|
||||
endpoints: endpoints,
|
||||
}
|
||||
go watcher.run(recv)
|
||||
return watcher
|
||||
}
|
||||
|
||||
func (w *vpnStatusWatcher[T]) run(recv func() ([]T, error)) {
|
||||
for {
|
||||
endpoints, err := recv()
|
||||
w.access.Lock()
|
||||
if err != nil {
|
||||
w.err = err
|
||||
} else {
|
||||
w.endpoints = endpoints
|
||||
}
|
||||
close(w.updated)
|
||||
w.updated = make(chan struct{})
|
||||
w.access.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *vpnStatusWatcher[T]) current() ([]T, <-chan struct{}, error) {
|
||||
w.access.Lock()
|
||||
defer w.access.Unlock()
|
||||
return w.endpoints, w.updated, w.err
|
||||
}
|
||||
|
||||
type interactiveReadResult struct {
|
||||
line string
|
||||
err error
|
||||
}
|
||||
|
||||
type interactiveReadRequest struct {
|
||||
prompt string
|
||||
hidden bool
|
||||
result chan interactiveReadResult
|
||||
}
|
||||
|
||||
type interactiveInput struct {
|
||||
requests chan interactiveReadRequest
|
||||
}
|
||||
|
||||
func newInteractiveInput() *interactiveInput {
|
||||
input := &interactiveInput{requests: make(chan interactiveReadRequest)}
|
||||
go input.run()
|
||||
return input
|
||||
}
|
||||
|
||||
func (i *interactiveInput) run() {
|
||||
for request := range i.requests {
|
||||
os.Stderr.WriteString(request.prompt)
|
||||
line, err := readTerminalLine(request.hidden)
|
||||
request.result <- interactiveReadResult{line: line, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func readTerminalLine(hidden bool) (string, error) {
|
||||
if hidden {
|
||||
line, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
os.Stderr.WriteString("\n")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(line), nil
|
||||
}
|
||||
var builder strings.Builder
|
||||
buffer := make([]byte, 1)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buffer)
|
||||
if n > 0 {
|
||||
if buffer[0] == '\n' {
|
||||
break
|
||||
}
|
||||
builder.WriteByte(buffer[0])
|
||||
}
|
||||
if err != nil {
|
||||
if builder.Len() == 0 {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(builder.String(), "\r"), nil
|
||||
}
|
||||
|
||||
type authPrompter struct {
|
||||
ctx context.Context
|
||||
input *interactiveInput
|
||||
once sync.Once
|
||||
aborted chan struct{}
|
||||
abortErr error
|
||||
}
|
||||
|
||||
func (p *authPrompter) abort(cause error) {
|
||||
p.once.Do(func() {
|
||||
p.abortErr = cause
|
||||
close(p.aborted)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *authPrompter) read(prompt string, hidden bool) (string, error) {
|
||||
result := make(chan interactiveReadResult, 1)
|
||||
select {
|
||||
case p.input.requests <- interactiveReadRequest{prompt: prompt, hidden: hidden, result: result}:
|
||||
case <-p.aborted:
|
||||
return "", p.abortErr
|
||||
case <-p.ctx.Done():
|
||||
return "", errAuthInterrupted
|
||||
}
|
||||
select {
|
||||
case value := <-result:
|
||||
return value.line, value.err
|
||||
case <-p.aborted:
|
||||
return "", p.abortErr
|
||||
case <-p.ctx.Done():
|
||||
return "", errAuthInterrupted
|
||||
}
|
||||
}
|
||||
|
||||
func (p *authPrompter) promptText(label string, value string) (string, error) {
|
||||
prompt := strings.TrimSuffix(label, ":")
|
||||
if value != "" {
|
||||
prompt += " [" + value + "]"
|
||||
}
|
||||
line, err := p.read(prompt+": ", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if line == "" {
|
||||
return value, nil
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (p *authPrompter) promptPassword(label string, value string) (string, error) {
|
||||
prompt := strings.TrimSuffix(label, ":")
|
||||
if value != "" {
|
||||
prompt += " (unchanged)"
|
||||
}
|
||||
line, err := p.read(prompt+": ", true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if line == "" {
|
||||
return value, nil
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (p *authPrompter) promptConfirm(prompt string) (bool, error) {
|
||||
line, err := p.read(prompt, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(line)) {
|
||||
case "", "y", "yes":
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func writeAuthLine(message string) {
|
||||
os.Stderr.WriteString(message + "\n")
|
||||
}
|
||||
|
||||
func writeAuthError(domain string, message string) {
|
||||
os.Stderr.WriteString(domain + " auth: " + message + "\n")
|
||||
}
|
||||
|
||||
func writeAuthHeader(endpointTag string, title string) {
|
||||
os.Stderr.WriteString("\n" + endpointTag + ": " + title + "\n")
|
||||
}
|
||||
|
||||
func writeAuthBanner(banner string) {
|
||||
var output strings.Builder
|
||||
for line := range strings.SplitSeq(strings.ReplaceAll(banner, "\r\n", "\n"), "\n") {
|
||||
output.WriteString(" " + line + "\n")
|
||||
}
|
||||
os.Stderr.WriteString(output.String())
|
||||
}
|
||||
|
||||
type submitOutcome int
|
||||
|
||||
const (
|
||||
submitRejected submitOutcome = iota
|
||||
submitStale
|
||||
submitFatal
|
||||
)
|
||||
|
||||
func classifySubmitError(err error) (submitOutcome, string) {
|
||||
grpcStatus, isStatus := status.FromError(err)
|
||||
if !isStatus {
|
||||
return submitFatal, err.Error()
|
||||
}
|
||||
switch grpcStatus.Code() {
|
||||
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded, codes.Unauthenticated, codes.Unimplemented:
|
||||
return submitFatal, grpcStatus.Message()
|
||||
}
|
||||
if strings.Contains(grpcStatus.Message(), "no pending") {
|
||||
return submitStale, grpcStatus.Message()
|
||||
}
|
||||
return submitRejected, grpcStatus.Message()
|
||||
}
|
||||
|
||||
func wrapAuthError(domain string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
_, isStatus := status.FromError(err)
|
||||
if isStatus {
|
||||
return err
|
||||
}
|
||||
return E.Cause(err, domain+" auth")
|
||||
}
|
||||
|
||||
func formatVPNConnectedSince(connectedSince int64) string {
|
||||
if connectedSince == 0 {
|
||||
return ""
|
||||
}
|
||||
since := time.Unix(connectedSince, 0).Local()
|
||||
return since.Format(time.RFC3339) + " (" + time.Since(since).Truncate(time.Second).String() + ")"
|
||||
}
|
||||
|
||||
func formatAuthDeadline(deadline int64) string {
|
||||
if deadline == 0 {
|
||||
return ""
|
||||
}
|
||||
return max(time.Until(time.Unix(deadline, 0)).Truncate(time.Second), 0).String()
|
||||
}
|
||||
|
||||
func isLoopbackHost(host string) bool {
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
address, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return address.IsLoopback()
|
||||
}
|
||||
|
||||
func warnPlaintextAPIConnection() {
|
||||
parsed, err := url.Parse(commandAPIServerURL)
|
||||
if err != nil || parsed.Scheme == "https" || isLoopbackHost(parsed.Hostname()) {
|
||||
return
|
||||
}
|
||||
writeAuthLine("warning: submitting single sign-on credentials over a plaintext API connection")
|
||||
}
|
||||
|
||||
func openURLInBrowser(target string) error {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return exec.Command("open", target).Start()
|
||||
case "windows":
|
||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", target).Start()
|
||||
default:
|
||||
return exec.Command("xdg-open", target).Start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIOpenConnectStatus = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Print OpenConnect endpoint status",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIOpenConnectStatus()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenConnect.AddCommand(commandAPIOpenConnectStatus)
|
||||
}
|
||||
|
||||
func runAPIOpenConnectStatus() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
defer cancel()
|
||||
_, endpoints, err := subscribeOpenConnectStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
writeStderrLine("no openconnect endpoint is configured")
|
||||
return nil
|
||||
}
|
||||
for index, endpointStatus := range endpoints {
|
||||
if index > 0 {
|
||||
os.Stdout.WriteString("\n")
|
||||
}
|
||||
writeOpenConnectStatusBlock(endpointStatus)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeOpenConnectStatusBlock(endpointStatus *daemon.OpenConnectEndpointStatus) {
|
||||
var block blockWriter
|
||||
block.addLine("Endpoint", endpointStatus.GetEndpointTag())
|
||||
block.addLine("State", endpointStatus.GetState())
|
||||
challenge := endpointStatus.GetAuthChallenge()
|
||||
tunnelInfo := endpointStatus.GetTunnelInfo()
|
||||
switch {
|
||||
case challenge != nil:
|
||||
block.addLine("Challenge", openConnectChallengeSummary(challenge))
|
||||
if challenge.GetMessage() != "" {
|
||||
block.addLine("Message", challenge.GetMessage())
|
||||
}
|
||||
if challenge.GetError() != "" {
|
||||
block.addLine("Error", challenge.GetError())
|
||||
}
|
||||
case tunnelInfo != nil:
|
||||
block.addLine("Server", tunnelInfo.GetServer())
|
||||
block.addLine("Flavor", tunnelInfo.GetFlavor())
|
||||
block.addLine("Transport", tunnelInfo.GetTransport())
|
||||
if len(tunnelInfo.GetIpv4()) > 0 {
|
||||
block.addLine("IPv4", strings.Join(tunnelInfo.GetIpv4(), ", "))
|
||||
}
|
||||
if len(tunnelInfo.GetIpv6()) > 0 {
|
||||
block.addLine("IPv6", strings.Join(tunnelInfo.GetIpv6(), ", "))
|
||||
}
|
||||
if len(tunnelInfo.GetDns()) > 0 {
|
||||
block.addLine("DNS", strings.Join(tunnelInfo.GetDns(), ", "))
|
||||
}
|
||||
if tunnelInfo.GetMtu() > 0 {
|
||||
block.addLine("MTU", F.ToString(tunnelInfo.GetMtu()))
|
||||
}
|
||||
block.addLine("Connected since", formatVPNConnectedSince(tunnelInfo.GetConnectedSince()))
|
||||
case endpointStatus.GetState() == adapter.OpenConnectStateError:
|
||||
block.addLine("Error", endpointStatus.GetError())
|
||||
}
|
||||
block.flush()
|
||||
if challenge != nil {
|
||||
writeStderrLine("")
|
||||
writeStderrLine(`run "sing-box api openconnect auth" to answer`)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user