sync upstream 1.14.0 and trim rr build

This commit is contained in:
2026-09-04 19:18:28 +03:00
parent 789522cef5
commit d786242bfd
1379 changed files with 180212 additions and 15188 deletions
+1 -1
View File
@@ -1 +1 @@
1778450375
1788503096
+1 -1
View File
@@ -1 +1 @@
1.13.18-rr-edition-2.6.5-r1
1.14.0-rr-edition-2.7.0-r1
+21 -4
View File
@@ -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
View File
@@ -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"
Binary file not shown.
+6 -2
View File
@@ -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
View File
@@ -1 +1 @@
e4926ba205fae5351e3d3eeafff7e7029654424a
45832ab074849607406baa3e3a2c4660274602ed
+2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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" ./...
+7
View File
@@ -23,3 +23,10 @@ AGENTS.md
/.claude/
dist
logs
/*.so
/*.log
/*.db-shm
/*.db-wal
/*.db.backup.*
/test_download.bin
/wget-log
+11 -19
View File
@@ -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$
+419 -4
View File
@@ -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
View File
@@ -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 \
+3 -1
View File
@@ -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)" \
+76 -38
View File
@@ -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
View File
@@ -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](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE)
[![go](https://img.shields.io/badge/go-1.26-00ADD8.svg)](go.mod)
[![codeberg](https://img.shields.io/badge/mirror-codeberg-2185D0.svg)](https://codeberg.org/shtorm-7/sing-box-extended)
[![telegram](https://img.shields.io/badge/telegram-chat-26A5E4.svg?logo=telegram)](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
+1
View File
@@ -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
}
+158
View File
@@ -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
}
+17
View File
@@ -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
View File
@@ -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 {
+8
View File
@@ -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()
+44 -19
View File
@@ -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()
}
+6 -32
View File
@@ -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
}
+43
View File
@@ -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()
}
}
+85 -10
View File
@@ -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)
}
+8
View File
@@ -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()
+45
View File
@@ -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())
}
+13 -4
View File
@@ -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())
+24
View File
@@ -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)
}
+5
View File
@@ -0,0 +1,5 @@
package adapter
type NetworkNamespaceManager interface {
ResolvePath(nameOrPath string) string
}
+7 -3
View File
@@ -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
}
+101
View File
@@ -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
}
+57
View File
@@ -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
}
+5 -5
View File
@@ -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 {
+2 -2
View File
@@ -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
+8
View File
@@ -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()
+47 -2
View File
@@ -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 {
+49 -11
View File
@@ -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)
}
}
}
+93 -53
View File
@@ -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
}
+9 -2
View File
@@ -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
+19
View File
@@ -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)
}
+8
View File
@@ -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()
+104
View File
@@ -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
}
+38 -38
View File
@@ -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
+59 -50
View File
@@ -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)
}
+15
View File
@@ -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))
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
package adapter
type USBIPDynamicServer interface {
usbipNotIncluded()
}
+197 -60
View File
@@ -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 == "" {
+26
View File
@@ -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
}
+13 -8
View File
@@ -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")
}
+165
View File
@@ -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
}
+2 -2
View File
@@ -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())
}
+4 -1
View File
@@ -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)
+107
View File
@@ -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
}
+29
View File
@@ -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
}
+96
View File
@@ -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
}
}
}
+39
View File
@@ -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