Compare commits

..
Author SHA1 Message Date
世界 61eff2c591 documentation: Bump version 2024-11-09 12:35:59 +08:00
世界 e4eacf32d8 documentation: Add rule action 2024-11-09 12:35:59 +08:00
世界 903de67b4d documentation: Update the scheduled removal time of deprecated features 2024-11-09 12:35:59 +08:00
世界 6ec669cde5 documentation: Remove outdated icons 2024-11-09 12:35:58 +08:00
世界 8742761e11 Migrate bad options to library 2024-11-09 12:35:58 +08:00
世界 322192a66b Implement udp connect 2024-11-09 12:35:58 +08:00
世界 ec4bf64f45 Implement new deprecated warnings 2024-11-09 12:35:57 +08:00
世界 4f99c669c9 Improve rule actions 2024-11-09 12:35:57 +08:00
世界 0e5ba64f44 Remove unused reject methods 2024-11-09 12:35:57 +08:00
世界 849e387d19 refactor: Modular inbounds/outbounds 2024-11-09 12:35:56 +08:00
世界 e45763d5ba Implement dns-hijack 2024-11-09 12:35:48 +08:00
世界 5eb8522205 Implement resolve(server) 2024-11-09 12:35:47 +08:00
世界 c2b833a228 Implement TCP and ICMP rejects 2024-11-09 12:35:47 +08:00
世界 7f65ab8166 Crazy sekai overturns the small pond 2024-11-09 12:35:43 +08:00
1921 changed files with 16720 additions and 297313 deletions
-35
View File
@@ -1,35 +0,0 @@
-s dir
--name sing-box-extended
--category net
--license GPL-3.0-or-later
--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
--depends ca-bundle
--depends kmod-inet-diag
--depends kmod-tun
--depends firewall4
--depends kmod-nft-queue
--before-remove release/config/openwrt.prerm
release/config/config.json=/etc/sing-box/config.json
release/config/openwrt.conf=/etc/config/sing-box
release/config/openwrt.init=/etc/init.d/sing-box
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE
-23
View File
@@ -1,23 +0,0 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--config-files etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE
-26
View File
@@ -1,26 +0,0 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--vendor SagerNet
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes
--config-files /etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE
-1
View File
@@ -1 +0,0 @@
45832ab074849607406baa3e3a2c4660274602ed
-2
View File
@@ -31,8 +31,6 @@ 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,8 +31,6 @@ 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)
- 其他
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
prepare_apk_root() {
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
APK_ROOT_DIR=$(mktemp -d)
mkdir -p "$APK_ROOT_DIR/etc"
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
EOF
cat > "$APK_ROOT_DIR/etc/group" <<EOF
root:x:$(id -g):root
EOF
}
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
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="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
prepare_apk_root
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box"
# Service files
install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service"
install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/conf.d/sing-box
/etc/init.d/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk --root "$APK_ROOT_DIR" mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later with name use or association addition" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
prepare_apk_root() {
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
APK_ROOT_DIR=$(mktemp -d)
mkdir -p "$APK_ROOT_DIR/etc"
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
EOF
cat > "$APK_ROOT_DIR/etc/group" <<EOF
root:x:$(id -g):root
EOF
}
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
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)
prepare_apk_root
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box"
install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/config/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk --root "$APK_ROOT_DIR" mkpkg \
--info "name:sing-box-extended" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform (extended)." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later" \
--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" \
--output "$OUTPUT_PATH"
-57
View File
@@ -1,57 +0,0 @@
#!/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
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
set -e -o pipefail
PROJECT=$(dirname "$0")/../..
TMP_PATH=`mktemp -d`
cp $2 $TMP_PATH
pushd $TMP_PATH
DEB_NAME=`ls *.deb`
ar x $DEB_NAME
mkdir control
pushd control
tar xf ../control.tar.gz
rm md5sums
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
cat control
tar czf ../control.tar.gz ./*
popd
DEB_NAME=${DEB_NAME%.deb}
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
popd
cp $TMP_PATH/$DEB_NAME.ipk $3
rm -r $TMP_PATH
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
branches=$(git branch -r --contains HEAD)
if echo "$branches" | grep -q 'origin/stable'; then
track=stable
elif echo "$branches" | grep -q 'origin/testing'; then
track=testing
elif echo "$branches" | grep -q 'origin/oldstable'; then
track=oldstable
else
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
exit 1
fi
if [[ "$track" == "stable" ]]; then
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
track=beta
fi
fi
case "$track" in
stable) name=sing-box; docker_tag=latest ;;
beta) name=sing-box-beta; docker_tag=latest-beta ;;
testing) name=sing-box-testing; docker_tag=latest-testing ;;
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
esac
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
echo "TRACK=${track}" >> "$GITHUB_ENV"
echo "NAME=${name}" >> "$GITHUB_ENV"
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
+1 -1
View File
@@ -6,7 +6,7 @@
":disableRateLimiting"
],
"baseBranches": [
"unstable"
"dev-next"
],
"golang": {
"enabled": false
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.26.7"
PATCH_COMMITS=(
"f080b0c6346eb690c0dc82497b35925f385b35ac"
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
"367663849c656612fc3c8875ef6e7f6b0c93dbd0"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz"
tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
#cp -a go go_bootstrap
mv go go_osx
cd go_osx
# 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
done
# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external
# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the
# stdlib (crypto/x509) is compiled from patched src automatically.
#cd src
#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash
#cd ../..
#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz"
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.26.7"
PATCH_COMMITS=(
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
"95b851f661584711faa8115b3234a461044f4510"
"b0bf0a863cf218b9bb0d6c013903ab160ae0c39b"
"7c7a2a0d68920f8764d9a3aeae7ad1067a17b3fa"
"ba34356e82f3c12a7303d2231e6ea0e42bbbb3bf"
"472a88edbc2a42feb53fa31d29ab54d33840dca6"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
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.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"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
# fixes:
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done
+1 -2
View File
@@ -4,7 +4,7 @@ PROJECTS=$(dirname "$0")/../..
function updateClient() {
pushd clients/$1
git fetch origin main
git fetch
git reset FETCH_HEAD --hard
popd
git add clients/$1
@@ -12,4 +12,3 @@ function updateClient() {
updateClient "apple"
updateClient "android"
updateClient "desktop"
-13
View File
@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
SCRIPT_DIR=$(dirname "$0")
PROJECTS=$SCRIPT_DIR/../..
git -C $PROJECTS/cronet-go fetch origin main
git -C $PROJECTS/cronet-go fetch origin go
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
go mod tidy
git -C $PROJECTS/cronet-go rev-parse origin/go > "$SCRIPT_DIR/CRONET_GO_VERSION"
-13
View File
@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
SCRIPT_DIR=$(dirname "$0")
PROJECTS=$SCRIPT_DIR/../..
git -C $PROJECTS/cronet-go fetch origin dev
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/go_dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
name: Debug build
on:
push:
branches:
- stable-next
- main-next
- dev-next
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/debug.yml'
pull_request:
branches:
- stable-next
- main-next
- dev-next
jobs:
build:
name: Debug build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Run Test
run: |
go test -v ./...
build_go120:
name: Debug build (Go 1.20)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.20
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go120-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go120
build_go121:
name: Debug build (Go 1.21)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.21
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go121-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
build_go122:
name: Debug build (Go 1.22)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.22
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go122-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
cross:
strategy:
matrix:
include:
# windows
- name: windows-amd64
goos: windows
goarch: amd64
goamd64: v1
- name: windows-amd64-v3
goos: windows
goarch: amd64
goamd64: v3
- name: windows-386
goos: windows
goarch: 386
- name: windows-arm64
goos: windows
goarch: arm64
- name: windows-arm32v7
goos: windows
goarch: arm
goarm: 7
# linux
- name: linux-amd64
goos: linux
goarch: amd64
goamd64: v1
- name: linux-amd64-v3
goos: linux
goarch: amd64
goamd64: v3
- name: linux-386
goos: linux
goarch: 386
- name: linux-arm64
goos: linux
goarch: arm64
- name: linux-armv5
goos: linux
goarch: arm
goarm: 5
- name: linux-armv6
goos: linux
goarch: arm
goarm: 6
- name: linux-armv7
goos: linux
goarch: arm
goarm: 7
- name: linux-mips-softfloat
goos: linux
goarch: mips
gomips: softfloat
- name: linux-mips-hardfloat
goos: linux
goarch: mips
gomips: hardfloat
- name: linux-mipsel-softfloat
goos: linux
goarch: mipsle
gomips: softfloat
- name: linux-mipsel-hardfloat
goos: linux
goarch: mipsle
gomips: hardfloat
- name: linux-mips64
goos: linux
goarch: mips64
- name: linux-mips64el
goos: linux
goarch: mips64le
- name: linux-s390x
goos: linux
goarch: s390x
# darwin
- name: darwin-amd64
goos: darwin
goarch: amd64
goamd64: v1
- name: darwin-amd64-v3
goos: darwin
goarch: amd64
goamd64: v3
- name: darwin-arm64
goos: darwin
goarch: arm64
# freebsd
- name: freebsd-amd64
goos: freebsd
goarch: amd64
goamd64: v1
- name: freebsd-amd64-v3
goos: freebsd
goarch: amd64
goamd64: v3
- name: freebsd-386
goos: freebsd
goarch: 386
- name: freebsd-arm64
goos: freebsd
goarch: arm64
fail-fast: true
runs-on: ubuntu-latest
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOAMD64: ${{ matrix.goamd64 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
CGO_ENABLED: 0
TAGS: with_clash_api,with_quic
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.21
- name: Build
id: build
run: make
+23 -186
View File
@@ -1,10 +1,6 @@
name: Publish Docker Images
on:
#push:
# branches:
# - stable
# - testing
release:
types:
- published
@@ -17,25 +13,20 @@ env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs:
build_binary:
name: Build binary
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
include:
# Naive-enabled builds (musl)
- { arch: amd64, naive: true, docker_platform: "linux/amd64" }
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
- { arch: "386", naive: true, docker_platform: "linux/386" }
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
- { arch: mipsle, gomips: softfloat, naive: true, docker_platform: "linux/mipsle" }
- { arch: riscv64, naive: true, docker_platform: "linux/riscv64" }
- { arch: loong64, naive: true, docker_platform: "linux/loong64" }
# Non-naive builds
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
- { arch: s390x, docker_platform: "linux/s390x" }
platform:
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64
- linux/386
- linux/ppc64le
- linux/riscv64
- linux/s390x
steps:
- name: Get commit to build
id: ref
@@ -48,147 +39,7 @@ jobs:
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
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 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
if: matrix.naive
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set version
run: |
set -xeuo pipefail
VERSION=$(go run ./cmd/internal/read_tag)
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
- name: Set build tags
run: |
set -xeuo pipefail
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build (naive)
if: matrix.naive
run: |
set -xeuo pipefail
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: |
set -xeuo pipefail
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
- name: Prepare artifact
run: |
platform=${{ matrix.docker_platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
# Rename binary to include arch info for Dockerfile.binary
BINARY_NAME="sing-box-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
fi
mv sing-box "${BINARY_NAME}"
echo "BINARY_NAME=${BINARY_NAME}" >> $GITHUB_ENV
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: ${{ env.BINARY_NAME }}
if-no-files-found: error
retention-days: 1
build_docker:
name: Build Docker image
runs-on: ubuntu-latest
needs:
- build_binary
strategy:
fail-fast: true
matrix:
include:
- { platform: "linux/amd64" }
- { platform: "linux/arm/v6" }
- { platform: "linux/arm/v7" }
- { platform: "linux/arm64" }
- { platform: "linux/386" }
# mipsle: no base Docker image available for this platform
- { platform: "linux/ppc64le" }
- { platform: "linux/riscv64" }
- { platform: "linux/s390x" }
- { platform: "linux/loong64", base_image: "ghcr.io/loong64/alpine:edge" }
steps:
- name: Get commit to build
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
@@ -196,16 +47,6 @@ jobs:
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Download binary
uses: actions/download-artifact@v5
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: .
- name: Prepare binary
run: |
# Find and make the binary executable
chmod +x sing-box-*
ls -la sing-box-*
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
@@ -227,9 +68,8 @@ jobs:
with:
platforms: ${{ matrix.platform }}
context: .
file: Dockerfile.binary
build-args: |
BASE_IMAGE=${{ matrix.base_image || 'alpine' }}
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
@@ -245,10 +85,9 @@ jobs:
if-no-files-found: error
retention-days: 1
merge:
if: github.event_name != 'push'
runs-on: ubuntu-latest
needs:
- build_docker
- build
steps:
- name: Get commit to build
id: ref
@@ -260,15 +99,15 @@ jobs:
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Detect track
run: bash .github/detect_track.sh
if [[ $ref == *"-"* ]]; then
latest=latest-beta
else
latest=latest
fi
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
@@ -282,15 +121,13 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
if: github.event_name != 'push'
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
if: github.event_name != 'push'
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
+11 -53
View File
@@ -3,77 +3,35 @@ name: Lint
on:
push:
branches:
- oldstable
- stable
- testing
- unstable
- stable-next
- main-next
- dev-next
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/lint.yml'
pull_request:
branches:
- oldstable
- stable
- testing
- unstable
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
cancel-in-progress: true
- stable-next
- main-next
- dev-next
jobs:
build:
name: Lint ${{ matrix.goos }}/${{ matrix.goarch }}
name: Build
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@v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.26
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
go-version: ^1.23
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=30m
install-mode: binary
verify: false
install-mode: binary
+20 -224
View File
@@ -1,243 +1,39 @@
name: Build Linux Packages
name: Release to Linux repository
on:
#push:
# branches:
# - stable
# - testing
workflow_dispatch:
inputs:
version:
description: "Version name"
required: true
type: string
release:
types:
- published
jobs:
calculate_version:
name: Calculate version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
echo "version=${{ inputs.version }}"
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs
id: outputs
run: |-
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build binary
runs-on: ubuntu-26.04
needs:
- calculate_version
strategy:
matrix:
include:
# Naive-enabled builds (musl)
- { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, debian: mipsel, rpm: mipsel }
- { os: linux, arch: riscv64, naive: true, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, naive: true, debian: loongarch64, rpm: loongarch64 }
# Non-naive builds (unsupported architectures)
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.7
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
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 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
if: matrix.naive
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
- name: Set tag
go-version: ^1.23
- name: Extract signing key
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Set build tags
run: |
set -xeuo pipefail
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build (naive)
if: matrix.naive
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set mtime
run: |-
TZ=UTC touch -t '197001010000' dist/sing-box
- name: Detect track
run: bash .github/detect_track.sh
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
set -xeuo pipefail
sudo gem install fpm
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
--name "${NAME}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
rm -rf $HOME/.gnupg
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }}
echo "HOME=$HOME" >> "$GITHUB_ENV"
EOF
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
- name: Package RPM
if: matrix.rpm != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
echo '%_rpmformat 4' > "$HOME/.rpmmacros"
cp .fpm_systemd .fpm
fpm -t rpm \
--name "${NAME}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box
cat > $HOME/.rpmmacros <<EOF
%_gpg_name ${{ secrets.GPG_KEY_ID }}
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
EOF
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
EOF
rpmsign --addsign dist/*.rpm
- name: Cleanup
run: rm dist/sing-box
- name: Upload artifact
uses: actions/upload-artifact@v4
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Publish release
uses: goreleaser/goreleaser-action@v6
with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
path: "dist"
upload:
name: Upload builds
runs-on: ubuntu-latest
needs:
- calculate_version
- build
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds
uses: actions/download-artifact@v5
with:
path: dist
merge-multiple: true
- name: Publish packages
if: github.event_name != 'push'
run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
distribution: goreleaser-pro
version: latest
args: release -f .goreleaser.fury.yaml --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
-57
View File
@@ -1,57 +0,0 @@
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" ./...
+1 -15
View File
@@ -12,21 +12,7 @@
/*.jar
/*.aar
/*.xcframework/
/experimental/libbox/*.aar
/experimental/libbox/*.xcframework/
/experimental/libbox/*.nupkg
.DS_Store
/config.d/
/venv/
CLAUDE.md
AGENTS.md
/.claude/
dist
logs
/*.so
/*.log
/*.db-shm
/*.db-wal
/*.db.backup.*
/test_download.bin
/wget-log
+6
View File
@@ -0,0 +1,6 @@
[submodule "clients/apple"]
path = clients/apple
url = https://github.com/SagerNet/sing-box-for-apple.git
[submodule "clients/android"]
path = clients/android
url = https://github.com/SagerNet/sing-box-for-android.git
+25 -53
View File
@@ -1,56 +1,28 @@
version: "2"
run:
go: "1.24"
build-tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
- with_ccm
- with_ocm
- badlinkname
- tfogo_checklinkname0
linters:
default: none
disable-all: true
enable:
- ineffassign
- staticcheck
- unused
- modernize
settings:
modernize:
disable:
- omitzero # nested struct omitempty -> omitzero changes JSON output semantics
staticcheck:
checks:
- all
- -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
paths:
- transport/simple-obfs
- \.pb\.go$
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
settings:
gci:
sections:
- standard
- prefix(github.com/sagernet/)
- default
custom-order: true
- govet
- gci
- staticcheck
- paralleltest
- ineffassign
linters-settings:
gci:
custom-order: true
sections:
- standard
- prefix(github.com/sagernet/)
- default
staticcheck:
checks:
- all
- -SA1003
run:
go: "1.23"
issues:
exclude-dirs:
- transport/simple-obfs
+96
View File
@@ -0,0 +1,96 @@
project_name: sing-box
builds:
- id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
env:
- CGO_ENABLED=0
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
mod_timestamp: '{{ .CommitTimestamp }}'
snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
nfpms:
- &template
id: package
package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
conflicts:
- sing-box-beta
- id: package_beta
<<: *template
package_name: sing-box-beta
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
formats:
- deb
- rpm
conflicts:
- sing-box
release:
disable: true
furies:
- account: sagernet
ids:
- package
disable: "{{ not (not .Prerelease) }}"
- account: sagernet
ids:
- package_beta
disable: "{{ not .Prerelease }}"
+94 -444
View File
@@ -11,29 +11,18 @@ builds:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
- -checklinkname=0
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- 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
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
@@ -42,13 +31,14 @@ builds:
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: mips
- id: legacy
<<: *template
tags:
- with_gvisor
@@ -56,29 +46,21 @@ builds:
- with_dhcp
- with_wireguard
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
- with_masque
- with_mtproxy
- with_trusttunnel
- with_call
- with_sudoku
- with_profiler
- badlinkname
- tfogo_checklinkname0
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go1.20.14
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
targets:
- linux_mips
- linux_mips_softfloat
- linux_mipsle
- linux_mipsle_softfloat
- linux_mips64
- linux_mips64le
- windows_amd64_v1
- windows_386
- darwin_amd64_v1
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
@@ -107,423 +89,98 @@ 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
builds:
- main
- mips
- android
formats:
- tar.gz
format: tar.gz
format_overrides:
- goos: windows
formats:
- zip
format: zip
wrap_in_directory: true
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-naive-purego-linux-amd64
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
<<: *template
builds:
- 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
- legacy
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
nfpms:
- id: package
package_name: sing-box
file_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 }}'
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'
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
- archlinux
# - apk
# - ipk
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
overrides:
apk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/sing-box.initd
dst: /etc/init.d/sing-box
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
ipk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/openwrt.init
dst: /etc/init.d/sing-box
- src: release/config/openwrt.conf
dst: /etc/config/sing-box
source:
enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
@@ -535,19 +192,12 @@ signs:
- artifacts: checksum
release:
github:
owner: shtorm-7
name: sing-box-extended
owner: SagerNet
name: sing-box
draft: true
prerelease: auto
mode: replace
ids:
- archive
- 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
- package
skip_upload: true
-24
View File
@@ -1,24 +0,0 @@
# Support the project
If you want to support the project, you can donate to the following addresses.
### TRX (Tron)
```
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
```
### TON
```
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
```
### Solana
```
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
```
### Bitcoin
```
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
```
### Ethereum
```
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
```
+14 -18
View File
@@ -1,31 +1,27 @@
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
LABEL maintainer="shtorm-7"
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box
ARG TARGETOS TARGETARCH
ARG GOPROXY=""
ARG CRONET_GO_PATH=/go/src/github.com/sagernet/sing-box/cronet-go
ENV GOPROXY=${GOPROXY}
ENV CGO_ENABLED=1
ENV GOPROXY ${GOPROXY}
ENV CGO_ENABLED=0
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
ENV CRONET_GO_PATH=${CRONET_GO_PATH}
RUN set -ex \
&& 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" \
&& apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" $LDFLAGS_SHARED -s -w -buildid=" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="shtorm-7"
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
&& apk upgrade \
&& apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]
-14
View File
@@ -1,14 +0,0 @@
ARG BASE_IMAGE=alpine
FROM ${BASE_IMAGE}
ARG TARGETARCH
ARG TARGETVARIANT
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& if command -v apk > /dev/null; then \
apk add --no-cache --upgrade bash tzdata ca-certificates nftables; \
else \
apt-get update && apt-get install -y --no-install-recommends bash tzdata ca-certificates nftables \
&& rm -rf /var/lib/apt/lists/*; \
fi
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]
+83 -195
View File
@@ -1,105 +1,77 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
LDFLAGS_SHARED = $(shell cat release/LDFLAGS)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
ADMIN_PANEL_DIR = service/admin_panel
ADMIN_PANEL_WEB = $(ADMIN_PANEL_DIR)/web
ADMIN_PANEL_DIST = $(ADMIN_PANEL_DIR)/dist
ADMIN_PANEL_TAGS = $(TAGS),with_admin_panel
DOCKER_IMAGE ?= shtorm7/sing-box-extended
DOCKER_PLATFORMS ?= linux/amd64,linux/arm64
CRONET_GO_PATH ?= $(shell pwd)/cronet-go
.PHONY: test release docs build schema
.PHONY: test release docs build
build:
go build $(MAIN_PARAMS) $(MAIN)
build_admin_panel:
cd $(ADMIN_PANEL_WEB) && \
npm install --no-fund --no-audit && \
npm run build
go run ./cmd/internal/admin_panel_pack \
-dir $(ADMIN_PANEL_DIST)
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 && \
go build -race $(MAIN_PARAMS) $(MAIN)
ci_build_go120:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build:
export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \
go build $(PARAMS) $(MAIN)
go build $(MAIN_PARAMS) $(MAIN)
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
go run -v --tags generate,generate_completions $(MAIN)
install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
fmt:
@golangci-lint fmt
@gofumpt -l -w .
@gofmt -s -w .
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
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
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
proto:
@go run ./cmd/internal/protogen
@golangci-lint fmt
@gofumpt -l -w .
@gofumpt -l -w .
proto_install:
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
update_certificates:
go run ./cmd/internal/update_certificates
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
release:
go run ./cmd/internal/build goreleaser release --clean --skip publish
mkdir dist/release
mv dist/*.tar.gz \
dist/*.zip \
dist/*.ipk \
dist/*.apk \
dist/*.deb \
dist/*.rpm \
dist/*_amd64.pkg.tar.zst \
dist/*_arm64.pkg.tar.zst \
dist/release
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
rm -r dist/release
release_repo:
@@ -108,81 +80,44 @@ release_repo:
release_install:
go install -v github.com/tcnksm/ghr@latest
release_docker:
sudo docker buildx build \
--platform $(DOCKER_PLATFORMS) \
-t $(DOCKER_IMAGE):latest \
-t $(DOCKER_IMAGE):$(VERSION) \
--push \
--network=host \
.
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
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :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
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
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
rm -rf dist/release_android
release_android: lib_android update_android_version build_android upload_android
publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
build_desktop_windows:
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:win
publish_android_appcenter:
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
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 -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 "❌"
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
upload_ios_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
build_ios_deb:
$(MAKE) -C ../sing-box-for-apple build_ios_deb
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 -derivedDataPath build/SFM.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
upload_macos_app_store:
cd ../sing-box-for-apple && \
@@ -191,103 +126,64 @@ upload_macos_app_store:
release_macos: build_macos upload_macos_app_store
build_macos_standalone:
$(MAKE) -C ../sing-box-for-apple archive_macos_standalone
cd ../sing-box-for-apple && \
rm -rf build/SFM.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
build_macos_dmg:
$(MAKE) -C ../sing-box-for-apple build_macos_dmg
build_macos_pkg:
$(MAKE) -C ../sing-box-for-apple build_macos_pkg
notarize_macos_dmg:
$(MAKE) -C ../sing-box-for-apple notarize_macos_dmg
notarize_macos_pkg:
$(MAKE) -C ../sing-box-for-apple notarize_macos_pkg
rm -rf dist/SFM
mkdir -p dist/SFM
cd ../sing-box-for-apple && \
rm -rf build/SFM.System && \
rm -rf build/SFM.dmg && \
xcodebuild -exportArchive \
-archivePath "build/SFM.System.xcarchive" \
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
-exportPath "build/SFM.System" && \
create-dmg \
--volname "sing-box" \
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
--icon "SFM.app" 0 0 \
--hide-extension "SFM.app" \
--app-drop-link 0 0 \
--skip-jenkins \
--notarize "notarytool-password" \
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
upload_macos_dmg:
mkdir -p dist/SFM
cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg"
cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg"
cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg"
upload_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 --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.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"
cd dist/SFM && \
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
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"
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
zip -r SFM.dSYMs.zip dSYMs && \
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
popd && \
cd dist/SFM && \
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.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
release_macos_standalone: build_macos_standalone build_macos_dmg upload_macos_dmg 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 -derivedDataPath build/SFT.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
upload_tvos_app_store:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
export_tvos_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \
cp build/SFT/sing-box.ipa dist/SFT.ipa
upload_tvos_ipa:
cd dist && \
cp SFT.ipa "SFT-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa"
release_tvos: build_tvos upload_tvos_app_store
update_apple_version:
go run ./cmd/internal/update_apple_version
update_macos_version:
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight:
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store
publish_app_store:
go run -v ./cmd/internal/app_store_connect publish_app_store
test:
@go test -v ./... && \
cd test && \
@@ -303,21 +199,16 @@ test_stdio:
lib_android:
go run ./cmd/internal/build_libbox -target android
lib_apple:
go run ./cmd/internal/build_libbox -target apple
lib_ios:
go run ./cmd/internal/build_libbox -target ios
lib_windows:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
lib_android_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
lib_apple_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
lib:
go run ./cmd/internal/build_libbox -target android
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.13
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.13
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
docs:
venv/bin/mkdocs serve
@@ -326,8 +217,8 @@ publish_docs:
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install:
python3 -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*"
python -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean:
rm -rf bin dist sing-box
@@ -336,7 +227,4 @@ clean:
update:
git fetch
git reset FETCH_HEAD --hard
git clean -fdx
%:
@:
git clean -fdx
+6 -91
View File
@@ -1,97 +1,12 @@
# sing-box-extended
# sing-box
[![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)
The universal proxy platform.
Sing-box with extended features.
[![Packaging status](https://repology.org/badge/vertical-allrepos/sing-box.svg)](https://repology.org/project/sing-box/versions)
## 🔥 Features
## Documentation
### 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
- **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
### DNS
- **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
- **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:
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
## Support the Project
If you want to support the project, you can donate to the following addresses.
#### Tribute
**[RUB Donate](https://web.tribute.tg/d/JxY)**
**[EUR Donate](https://web.tribute.tg/d/JxZ)**
**[USD Donate](https://web.tribute.tg/d/Jy1)**
#### TRX (Tron)
```
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
```
#### TON
```
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
```
#### Solana
```
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
```
#### Bitcoin
```
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
```
#### Ethereum
```
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
```
https://sing-box.sagernet.org
## License
@@ -113,4 +28,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
In addition, no derivative work may use the name or imply association
with this application without prior consent.
```
```
-22
View File
@@ -1,22 +0,0 @@
package adapter
import (
"context"
"crypto/x509"
"github.com/sagernet/sing/service"
)
type CertificateStore interface {
LifecycleService
Pool() *x509.CertPool
ExclusiveAnchors() bool
}
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
store := service.FromContext[CertificateStore](ctx)
if store == nil {
return nil
}
return store.Pool()
}
-21
View File
@@ -1,21 +0,0 @@
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
@@ -1,158 +0,0 @@
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
}
-80
View File
@@ -1,80 +0,0 @@
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
@@ -1,17 +0,0 @@
//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
}
-38
View File
@@ -1,38 +0,0 @@
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
}
-18
View File
@@ -1,18 +0,0 @@
package adapter
import (
"context"
"net"
N "github.com/sagernet/sing/common/network"
)
type ConnectionManager interface {
Lifecycle
Count() int
CloseAll()
TrackConn(conn net.Conn) net.Conn
TrackPacketConn(conn net.PacketConn) net.PacketConn
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
-116
View File
@@ -1,116 +0,0 @@
package adapter
import (
"context"
"net/netip"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
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)
ResetNetwork()
}
type DNSClient interface {
Start()
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
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 || options.Server == "" {
return DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server)
}
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
}
type RDRCStore interface {
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
SaveRDRC(transportName string, qName string, qType uint16) error
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
Tag() string
Dependencies() []string
// Reset closes the transport's existing connections so later requests use fresh connections.
// 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 DNSTransportWithPreferredDomain interface {
DNSTransport
PreferredDomain(domain string) bool
}
type DNSTransportWithEnvironment interface {
DNSTransport
Environment() []string
}
type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
}
type DNSTransportManager interface {
Lifecycle
Transports() []DNSTransport
Transport(tag string) (DNSTransport, bool)
Default() DNSTransport
FakeIP() FakeIPTransport
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
}
-28
View File
@@ -1,28 +0,0 @@
package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Endpoint interface {
Lifecycle
Type() string
Tag() string
Outbound
}
type EndpointRegistry interface {
option.EndpointOptionsRegistry
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error)
}
type EndpointManager interface {
Lifecycle
Endpoints() []Endpoint
Get(tag string) (Endpoint, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
}
-43
View File
@@ -1,43 +0,0 @@
package endpoint
import "github.com/sagernet/sing-box/option"
type Adapter struct {
endpointType string
endpointTag string
network []string
dependencies []string
}
func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter {
return Adapter{
endpointType: endpointType,
endpointTag: endpointTag,
network: network,
dependencies: dependencies,
}
}
func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter {
var dependencies []string
if dialOptions.Detour != "" {
dependencies = []string{dialOptions.Detour}
}
return NewAdapter(endpointType, endpointTag, network, dependencies)
}
func (a *Adapter) Type() string {
return a.endpointType
}
func (a *Adapter) Tag() string {
return a.endpointTag
}
func (a *Adapter) Network() []string {
return a.network
}
func (a *Adapter) Dependencies() []string {
return a.dependencies
}
-158
View File
@@ -1,158 +0,0 @@
package endpoint
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.EndpointManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.EndpointRegistry
access sync.Mutex
started bool
stage adapter.StartStage
endpoints []adapter.Endpoint
endpointByTag map[string]adapter.Endpoint
}
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpointByTag: make(map[string]adapter.Endpoint),
}
}
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
if stage == adapter.StartStateStart {
// started with outbound manager
m.access.Unlock()
return nil
}
endpoints := m.endpoints
m.access.Unlock()
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(endpoint, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
endpoints := m.endpoints
m.endpoints = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Endpoints() []adapter.Endpoint {
m.access.Lock()
defer m.access.Unlock()
return m.endpoints
}
func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
m.access.Lock()
defer m.access.Unlock()
endpoint, found := m.endpointByTag[tag]
return endpoint, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
endpoint, found := m.endpointByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.endpointByTag, tag)
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == endpoint
})
if index == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return endpoint.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(endpoint, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
if m.started {
err = existsEndpoint.Close()
if err != nil {
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
}
}
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == existsEndpoint
})
if existsIndex == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
}
m.endpoints = append(m.endpoints, endpoint)
m.endpointByTag[tag] = endpoint
return nil
}
-80
View File
@@ -1,80 +0,0 @@
package endpoint
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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.EndpointRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, 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(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
}
+43 -96
View File
@@ -4,52 +4,34 @@ import (
"bytes"
"context"
"encoding/binary"
"io"
"net"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/varbin"
)
type ClashServer interface {
LifecycleService
Service
PreStarter
Mode() string
ModeList() []string
SetMode(mode string)
AddModeUpdateHook(hook *observable.Subscriber[struct{}])
}
type URLTestHistory struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
}
type V2RayServer interface {
LifecycleService
StatsService() ConnectionTracker
HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
}
type CacheFile interface {
LifecycleService
CacheID() string
Service
PreStarter
StoreFakeIP() bool
FakeIPStorage
StoreRDRC() bool
RDRCStore
StoreWARPConfig() bool
StoreMASQUEConfig() bool
StoreProviders() bool
StoreDNS() bool
DNSCacheStore
SetDisableExpire(disableExpire bool)
SetOptimisticTimeout(timeout time.Duration)
dns.RDRCStore
LoadMode() string
StoreMode(mode string) error
@@ -57,34 +39,23 @@ type CacheFile interface {
StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error
LoadWARPConfig(tag string) *SavedBinary
SaveWARPConfig(tag string, set *SavedBinary) error
LoadMASQUEConfig(tag string) *SavedBinary
SaveMASQUEConfig(tag string, set *SavedBinary) error
LoadSubscription(tag string) *SavedBinary
SaveSubscription(tag string, sub *SavedBinary) error
LoadRuleSet(tag string) *SavedRuleSet
SaveRuleSet(tag string, set *SavedRuleSet) error
}
type SavedBinary struct {
type SavedRuleSet struct {
Content []byte
LastUpdated time.Time
LastEtag string
URLHash []byte
}
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, uint8(2))
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
if err != nil {
return nil, err
}
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
if err != nil {
return nil, err
}
_, err = buffer.Write(s.Content)
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil {
return nil, err
}
@@ -92,41 +63,21 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
if err != nil {
return nil, err
}
_, err = buffer.WriteString(s.LastEtag)
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)
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data)
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)
if err != nil {
return err
}
contentLength, err := binary.ReadUvarint(reader)
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)
err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
@@ -136,37 +87,17 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
return err
}
s.LastUpdated = time.Unix(lastUpdated, 0)
etagLength, err := binary.ReadUvarint(reader)
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)
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil {
return err
}
return nil
}
type Tracker interface {
Leave()
}
type OutboundGroup interface {
Outbound
Now() string
@@ -176,5 +107,21 @@ type OutboundGroup interface {
type URLTestGroup interface {
OutboundGroup
URLTest(ctx context.Context) (map[string]uint16, error)
PerformUpdateCheck()
}
func OutboundTag(detour Outbound) string {
if group, isGroup := detour.(OutboundGroup); isGroup {
return group.Now()
}
return detour.Tag()
}
type V2RayServer interface {
Service
StatsService() V2RayStatsService
}
type V2RayStatsService interface {
RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
}
+3 -2
View File
@@ -3,11 +3,12 @@ package adapter
import (
"net/netip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/logger"
)
type FakeIPStore interface {
SimpleLifecycle
Service
Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool)
@@ -26,6 +27,6 @@ type FakeIPStorage interface {
}
type FakeIPTransport interface {
DNSTransport
dns.Transport
Store() FakeIPStore
}
+29 -6
View File
@@ -5,31 +5,54 @@ 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, onClose N.CloseHandlerFunc)
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
}
type ConnectionHandlerEx interface {
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
// Deprecated: use PacketHandlerEx instead
type PacketHandler interface {
NewPacket(buffer *buf.Buffer, source M.Socksaddr)
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
}
type PacketBatchHandler interface {
NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr)
type PacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
}
// Deprecated: use OOBPacketHandlerEx instead
type OOBPacketHandler interface {
NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
}
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, onClose N.CloseHandlerFunc)
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
type PacketConnectionHandlerEx interface {
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
type UpstreamHandlerAdapter interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
E.Handler
}
type UpstreamHandlerAdapterEx interface {
N.TCPConnectionHandlerEx
N.UDPConnectionHandlerEx
}
-43
View File
@@ -1,43 +0,0 @@
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()
}
}
+21 -118
View File
@@ -2,47 +2,33 @@ 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/common/process"
"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 {
Lifecycle
Service
Type() string
Tag() string
}
type TCPInjectableInbound interface {
Inbound
ConnectionHandler
ConnectionHandlerEx
}
type UDPInjectableInbound interface {
Inbound
PacketConnectionHandler
PacketConnectionHandlerEx
}
type InboundRegistry interface {
option.InboundOptionsRegistry
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
UnsafeCreate(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
}
type InboundManager interface {
Lifecycle
Inbounds() []Inbound
Get(tag string) (Inbound, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error)
}
type InboundContext struct {
@@ -52,59 +38,35 @@ type InboundContext struct {
Network string
Source M.Socksaddr
Destination M.Socksaddr
Gateway *netip.Addr
User string
Outbound string
// power report
RouteRule string
RouteOutbound string
// sniffer
Protocol string
Domain string
Client string
SniffContext any
SnifferNames []string
SniffError error
// cache
// Deprecated: implement in rule action
InboundDetour string
LastInbound string
OriginDestination M.Socksaddr
RouteOriginalDestination M.Socksaddr
InboundDetour string
LastInbound string
OriginDestination M.Socksaddr
// Deprecated
InboundOptions option.InboundOptions
UDPDisableDomainUnmapping bool
UDPConnect bool
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
TLSSpoof string
TLSSpoofMethod tlsspoof.Method
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DNSServer string
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
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *process.Info
QueryType uint16
FakeIP bool
// rule cache
@@ -115,82 +77,22 @@ type InboundContext struct {
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DeferredIPCIDRMatchGroups uint8
DidMatch bool
IgnoreDestinationIPCIDRMatch bool
}
func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.ResetRuleMatchCache()
}
func (c *InboundContext) ResetRuleMatchCache() {
c.SourceAddressMatch = false
c.SourcePortMatch = false
c.DestinationAddressMatch = false
c.DestinationPortMatch = 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
c.DidMatch = false
}
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)
}
@@ -213,7 +115,8 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
func OverrideContext(ctx context.Context) context.Context {
if metadata := ContextFrom(ctx); metadata != nil {
newMetadata := *metadata
var newMetadata InboundContext
newMetadata = *metadata
return WithContext(ctx, &newMetadata)
}
return ctx
-158
View File
@@ -1,158 +0,0 @@
package inbound
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.InboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.InboundRegistry
endpoint adapter.EndpointManager
access sync.Mutex
started bool
stage adapter.StartStage
inbounds []adapter.Inbound
inboundByTag map[string]adapter.Inbound
}
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpoint: endpoint,
inboundByTag: make(map[string]adapter.Inbound),
}
}
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
inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(inbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
inbounds := m.inbounds
m.inbounds = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Inbounds() []adapter.Inbound {
m.access.Lock()
defer m.access.Unlock()
return m.inbounds
}
func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
m.access.Lock()
inbound, found := m.inboundByTag[tag]
m.access.Unlock()
if found {
return inbound, true
}
return m.endpoint.Get(tag)
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
inbound, found := m.inboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.inboundByTag, tag)
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == inbound
})
if index == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return inbound.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(inbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
if m.started {
err = existsInbound.Close()
if err != nil {
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
}
}
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == existsInbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
}
m.inbounds = append(m.inbounds, inbound)
m.inboundByTag[tag] = inbound
return nil
}
+20 -36
View File
@@ -2,8 +2,6 @@ package inbound
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -17,12 +15,8 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
})
}
@@ -34,51 +28,41 @@ type (
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc),
optionsType: make(map[string]optionsConstructorFunc),
constructors: 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(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
m.access.Lock()
defer m.access.Unlock()
return m.UnsafeCreate(ctx, router, logger, tag, outboundType, options)
}
func (m *Registry) UnsafeCreate(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
constructor, loaded := m.constructor[outboundType]
func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[outboundType] = optionsConstructor
r.constructors[outboundType] = constructor
}
-45
View File
@@ -1,45 +0,0 @@
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())
}
-123
View File
@@ -1,123 +0,0 @@
package adapter
import (
"context"
"reflect"
"strings"
"time"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
type SimpleLifecycle interface {
Start() error
Close() error
}
type StartStage uint8
const (
StartStateInitialize StartStage = iota
StartStateStart
StartStatePostStart
StartStateStarted
)
var ListStartStages = []StartStage{
StartStateInitialize,
StartStateStart,
StartStatePostStart,
StartStateStarted,
}
func (s StartStage) String() string {
switch s {
case StartStateInitialize:
return "initialize"
case StartStateStart:
return "start"
case StartStatePostStart:
return "post-start"
case StartStateStarted:
return "finish-start"
default:
panic("unknown stage")
}
}
type Lifecycle interface {
Start(stage StartStage) error
Close() error
}
type LifecycleService interface {
Name() string
Lifecycle
}
func getServiceName(service any) string {
if named, ok := service.(interface {
Type() string
Tag() string
}); ok {
tag := named.Tag()
if tag != "" {
return named.Type() + "[" + tag + "]"
}
return named.Type()
}
t := reflect.TypeOf(service)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return strings.ToLower(t.Name())
}
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)
done()
if err != nil {
return err
}
}
return nil
}
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)
done()
if err != nil {
return E.Cause(err, stage.String(), " ", service.Name())
}
}
return nil
}
func LogElapsed(logger log.ContextLogger, description ...any) func() {
prefix := F.ToString(description...)
startTime := time.Now()
timer := time.AfterFunc(time.Second, func() {
logger.Trace(prefix, "...")
})
return func() {
if timer.Stop() {
return
}
logger.Trace(prefix, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
-52
View File
@@ -1,52 +0,0 @@
package adapter
func LegacyStart(starter any, stage StartStage) error {
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
return lifecycle.Start(stage)
}
switch stage {
case StartStateInitialize:
if preStarter, isPreStarter := starter.(interface {
PreStart() error
}); isPreStarter {
return preStarter.PreStart()
}
case StartStateStart:
if starter, isStarter := starter.(interface {
Start() error
}); isStarter {
return starter.Start()
}
case StartStateStarted:
if postStarter, isPostStarter := starter.(interface {
PostStart() error
}); isPostStarter {
return postStarter.PostStart()
}
}
return nil
}
type lifecycleServiceWrapper struct {
SimpleLifecycle
name string
}
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{
SimpleLifecycle: service,
name: name,
}
}
func (l *lifecycleServiceWrapper) Name() string {
return l.name
}
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.SimpleLifecycle, stage)
}
func (l *lifecycleServiceWrapper) Close() error {
return l.SimpleLifecycle.Close()
}
-24
View File
@@ -1,24 +0,0 @@
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
@@ -1,5 +0,0 @@
package adapter
type NetworkNamespaceManager interface {
ResolvePath(nameOrPath string) string
}
-85
View File
@@ -1,85 +0,0 @@
package adapter
import (
"context"
"encoding/hex"
"net"
"net/netip"
"strings"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
)
type NetworkManager interface {
Lifecycle
Initialize(ruleSets []RuleSet)
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultNetworkInterface() *NetworkInterface
NetworkInterfaces() []NetworkInterface
NetworkEnvironment() uint64
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
ProtectFunc() control.Func
DefaultOptions() NetworkOptions
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
AutoRedirectOutputMarkFunc() control.Func
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
NeedWIFIState() bool
WIFIState() WIFIState
UpdateWIFIState(ctx context.Context)
ResetNetwork(ctx context.Context)
}
type NetworkOptions struct {
BindInterface string
RoutingMark uint32
DomainResolver string
DomainResolveOptions DNSQueryOptions
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
}
type InterfaceUpdateListener interface {
InterfaceUpdated(ctx context.Context)
}
type WIFIState struct {
SSID string
BSSID string
}
func NormalizeWIFIBSSID(bssid string) string {
bssid = strings.TrimSpace(bssid)
if bssid == "" {
return ""
}
parsed, err := net.ParseMAC(bssid)
if err == nil && len(parsed) == 6 {
return parsed.String()
}
if len(bssid) == 12 {
decoded, err := hex.DecodeString(bssid)
if err == nil {
return net.HardwareAddr(decoded).String()
}
}
return bssid
}
type NetworkInterface struct {
control.Interface
Type C.InterfaceType
DNSServers []string
Gateways []netip.Addr
Expensive bool
Constrained bool
}
-101
View File
@@ -1,101 +0,0 @@
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
@@ -1,57 +0,0 @@
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
}
-29
View File
@@ -2,11 +2,9 @@ package adapter
import (
"context"
"net/netip"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
)
@@ -20,34 +18,7 @@ type Outbound interface {
N.Dialer
}
type OutboundWithPreferredRoutes interface {
Outbound
PreferredDomain(metadata *InboundContext, domain string) bool
PreferredAddress(metadata *InboundContext, address netip.Addr) bool
}
type OutboundWithMultiplex interface {
Outbound
MultiplexEnabled() bool
}
type FlowOutbound interface {
Outbound
tun.Port
PreMatchFlow(network string, destination netip.Addr) PreMatchAction
}
type OutboundRegistry interface {
option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
UnsafeCreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
}
type OutboundManager interface {
Lifecycle
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
Default() Outbound
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
}
+9 -9
View File
@@ -5,35 +5,35 @@ import (
)
type Adapter struct {
outboundType string
outboundTag string
protocol string
network []string
tag string
dependencies []string
}
func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter {
func NewAdapter(protocol string, network []string, tag string, dependencies []string) Adapter {
return Adapter{
outboundType: outboundType,
outboundTag: outboundTag,
protocol: protocol,
network: network,
tag: tag,
dependencies: dependencies,
}
}
func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter {
func NewAdapterWithDialerOptions(protocol string, network []string, tag string, dialOptions option.DialerOptions) Adapter {
var dependencies []string
if dialOptions.Detour != "" {
dependencies = []string{dialOptions.Detour}
}
return NewAdapter(outboundType, outboundTag, network, dependencies)
return NewAdapter(protocol, network, tag, dependencies)
}
func (a *Adapter) Type() string {
return a.outboundType
return a.protocol
}
func (a *Adapter) Tag() string {
return a.outboundTag
return a.tag
}
func (a *Adapter) Network() []string {
+245
View File
@@ -0,0 +1,245 @@
package outbound
import (
"context"
"net"
"net/netip"
"os"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/canceler"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn
var err error
if len(metadata.DestinationAddresses) > 0 {
outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
} else {
outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
err = N.ReportConnHandshakeSuccess(conn, outConn)
if err != nil {
outConn.Close()
return err
}
return CopyEarlyConn(ctx, conn, outConn)
}
func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn
var err error
if len(metadata.DestinationAddresses) > 0 {
outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses)
} else if metadata.Destination.IsFqdn() {
var destinationAddresses []netip.Addr
destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy)
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, destinationAddresses)
} else {
outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
err = N.ReportConnHandshakeSuccess(conn, outConn)
if err != nil {
outConn.Close()
return err
}
return CopyEarlyConn(ctx, conn, outConn)
}
func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata)
var (
outPacketConn net.PacketConn
outConn net.Conn
destinationAddress netip.Addr
err error
)
if metadata.UDPConnect {
if len(metadata.DestinationAddresses) > 0 {
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses)
} else {
outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
outPacketConn = bufio.NewUnbindPacketConn(outConn)
connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr())
if connRemoteAddr != metadata.Destination.Addr {
destinationAddress = connRemoteAddr
}
} else {
if len(metadata.DestinationAddresses) > 0 {
outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
} else {
outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
}
err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn)
if err != nil {
outPacketConn.Close()
return err
}
if destinationAddress.IsValid() {
if metadata.Destination.IsFqdn() {
if metadata.UDPDisableDomainUnmapping {
outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
} else {
outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
}
}
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
natConn.UpdateDestination(destinationAddress)
}
}
switch metadata.Protocol {
case C.ProtocolSTUN:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.STUNTimeout)
case C.ProtocolQUIC:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.QUICTimeout)
case C.ProtocolDNS:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout)
}
return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn))
}
func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata)
var (
outPacketConn net.PacketConn
outConn net.Conn
destinationAddress netip.Addr
err error
)
if metadata.UDPConnect {
if len(metadata.DestinationAddresses) > 0 {
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses)
} else if metadata.Destination.IsFqdn() {
var destinationAddresses []netip.Addr
destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy)
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, destinationAddresses)
} else {
outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr())
if connRemoteAddr != metadata.Destination.Addr {
destinationAddress = connRemoteAddr
}
} else {
if len(metadata.DestinationAddresses) > 0 {
outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses)
} else if metadata.Destination.IsFqdn() {
var destinationAddresses []netip.Addr
destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy)
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses)
} else {
outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
}
err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn)
if err != nil {
outPacketConn.Close()
return err
}
if destinationAddress.IsValid() {
if metadata.Destination.IsFqdn() {
outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
}
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
natConn.UpdateDestination(destinationAddress)
}
}
switch metadata.Protocol {
case C.ProtocolSTUN:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.STUNTimeout)
case C.ProtocolQUIC:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.QUICTimeout)
case C.ProtocolDNS:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout)
}
return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn))
}
func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error {
if cachedReader, isCached := conn.(N.CachedReader); isCached {
payload := cachedReader.ReadCached()
if payload != nil && !payload.IsEmpty() {
_, err := serverConn.Write(payload.Bytes())
payload.Release()
if err != nil {
serverConn.Close()
return err
}
return bufio.CopyConn(ctx, conn, serverConn)
}
}
if earlyConn, isEarlyConn := common.Cast[N.EarlyConn](serverConn); isEarlyConn && earlyConn.NeedHandshake() {
payload := buf.NewPacket()
err := conn.SetReadDeadline(time.Now().Add(C.ReadPayloadTimeout))
if err != os.ErrInvalid {
if err != nil {
payload.Release()
serverConn.Close()
return err
}
_, err = payload.ReadOnceFrom(conn)
if err != nil && !E.IsTimeout(err) {
payload.Release()
serverConn.Close()
return E.Cause(err, "read payload")
}
err = conn.SetReadDeadline(time.Time{})
if err != nil {
payload.Release()
serverConn.Close()
return err
}
}
_, err = serverConn.Write(payload.Bytes())
payload.Release()
if err != nil {
serverConn.Close()
return N.ReportHandshakeFailure(conn, err)
}
}
return bufio.CopyConn(ctx, conn, serverConn)
}
-310
View File
@@ -1,310 +0,0 @@
package outbound
import (
"context"
"io"
"os"
"strings"
"sync"
"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"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.OutboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.OutboundRegistry
endpoint adapter.EndpointManager
defaultTag string
access sync.RWMutex
started bool
stage adapter.StartStage
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
dependByTag map[string][]string
defaultOutbound adapter.Outbound
defaultOutboundFallback func() (adapter.Outbound, error)
}
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpoint: endpoint,
defaultTag: defaultTag,
outboundByTag: make(map[string]adapter.Outbound),
dependByTag: make(map[string][]string),
}
}
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) {
m.defaultOutboundFallback = defaultOutboundFallback
}
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
if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag)
}
m.defaultOutbound = defaultEndpoint
}
if m.defaultOutbound == nil {
directOutbound, err := m.defaultOutboundFallback()
if err != nil {
m.access.Unlock()
return E.Cause(err, "create direct outbound for fallback")
}
m.outbounds = append(m.outbounds, directOutbound)
m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound
}
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(outbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
return nil
}
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, outboundToStart := range outbounds {
outboundTag := outboundToStart.Tag()
if started[outboundTag] {
continue
}
dependencies := outboundToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[outboundTag] = true
canContinue = true
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name)
monitor.Start("start ", name)
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
done()
if err != nil {
return E.Cause(err, "start ", name)
}
} else if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name)
monitor.Start("start ", name)
err := starter.Start()
monitor.Finish()
done()
if err != nil {
return E.Cause(err, "start ", name)
}
}
}
if len(started) == len(outbounds) {
break
}
if canContinue {
continue
}
currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
return !started[it.Tag()]
})
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
m.access.Lock()
problemOutbound := m.outboundByTag[problemOutboundTag]
m.access.Unlock()
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}
return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound)
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
outbounds := m.outbounds
m.outbounds = nil
m.access.Unlock()
var err error
for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
}
return nil
}
func (m *Manager) Outbounds() []adapter.Outbound {
m.access.RLock()
defer m.access.RUnlock()
return m.outbounds
}
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
m.access.RLock()
outbound, found := m.outboundByTag[tag]
m.access.RUnlock()
if found {
return outbound, true
}
return m.endpoint.Get(tag)
}
func (m *Manager) Default() adapter.Outbound {
m.access.RLock()
defer m.access.RUnlock()
return m.defaultOutbound
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
defer m.access.Unlock()
outbound, found := m.outboundByTag[tag]
if !found {
return os.ErrInvalid
}
delete(m.outboundByTag, tag)
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == outbound
})
if index == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
started := m.started
if m.defaultOutbound == outbound {
if len(m.outbounds) > 0 {
m.defaultOutbound = m.outbounds[0]
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
} else {
m.defaultOutbound = nil
}
}
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
}
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
if len(m.dependByTag[dependency]) == 1 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
}
}
if started {
return common.Close(outbound)
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options)
if err != nil {
return err
}
if m.started {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(outbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
m.access.Lock()
defer m.access.Unlock()
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
if m.started {
err = common.Close(existsOutbound)
if err != nil {
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
}
}
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == existsOutbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
}
m.outbounds = append(m.outbounds, outbound)
m.outboundByTag[tag] = outbound
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
}
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
m.defaultOutbound = outbound
if m.started {
m.logger.Notice("updated default outbound to ", outbound.Tag())
}
}
return nil
}
+2 -18
View File
@@ -2,8 +2,6 @@ package outbound
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -17,12 +15,8 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
})
}
@@ -46,12 +40,6 @@ 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()
@@ -65,10 +53,6 @@ func (r *Registry) CreateOptions(outboundType string) (any, bool) {
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
r.access.Lock()
defer r.access.Unlock()
return r.UnsafeCreateOutbound(ctx, router, logger, tag, outboundType, options)
}
func (r *Registry) UnsafeCreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
-119
View File
@@ -1,119 +0,0 @@
package adapter
import (
"context"
"net/netip"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
)
type PlatformInterface interface {
Initialize(networkManager NetworkManager) error
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
UsePlatformNetworkInterfaces() bool
NetworkInterfaces() ([]NetworkInterface, error)
UnderNetworkExtension() bool
NetworkExtensionIncludeAllNetworks() bool
ClearDNSCache()
RequestPermissionForWIFIState() error
ReadWIFIState(ctx context.Context) WIFIState
UsePlatformConnectionOwnerFinder() bool
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
UsePlatformWIFIMonitor() bool
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 {
IpProtocol int32
SourceAddress string
SourcePort int32
DestinationAddress string
DestinationPort int32
}
type ConnectionOwner struct {
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageNames []string
}
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}
type SystemProxyStatus struct {
Available bool
Enabled bool
}
+8
View File
@@ -1 +1,9 @@
package adapter
type PreStarter interface {
PreStart() error
}
type PostStarter interface {
PostStart() error
}
-51
View File
@@ -1,51 +0,0 @@
package adapter
import (
"context"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/x/list"
)
type Provider interface {
Type() string
Tag() string
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
UpdatedAt() time.Time
HealthCheck(ctx context.Context) (map[string]uint16, error)
RegisterCallback(callback ProviderUpdateCallback) *list.Element[ProviderUpdateCallback]
UnregisterCallback(element *list.Element[ProviderUpdateCallback])
}
type ProviderUpdater interface {
Update() error
}
type ProviderSubscriptionInfo interface {
SubscriptionInfo() SubscriptionInfo
}
type ProviderRegistry interface {
option.ProviderOptionsRegistry
CreateProvider(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) (Provider, error)
}
type ProviderManager interface {
Lifecycle
Providers() []Provider
Get(tag string) (Provider, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) error
}
type SubscriptionInfo struct {
Upload int64
Download int64
Total int64
Expire int64
}
type ProviderUpdateCallback = func(tag string) error
-305
View File
@@ -1,305 +0,0 @@
package provider
import (
"context"
"reflect"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
)
type Adapter struct {
ctx context.Context
outbound adapter.OutboundManager
router adapter.Router
logFactory log.Factory
logger log.ContextLogger
providerType string
providerTag string
outbounds []adapter.Outbound
outboundsByTag map[string]adapter.Outbound
ticker *time.Ticker
checking atomic.Bool
history *urltest.HistoryStorage
callbackAccess sync.Mutex
callbacks list.List[adapter.ProviderUpdateCallback]
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 {
timeout := time.Duration(options.Timeout)
if timeout == 0 {
timeout = 3 * time.Second
}
interval := time.Duration(options.Interval)
if interval == 0 {
interval = 10 * time.Minute
}
if interval < time.Minute {
interval = time.Minute
}
return Adapter{
ctx: ctx,
outbound: outbound,
router: router,
logFactory: logFactory,
logger: logger,
providerType: providerType,
providerTag: providerTag,
enabled: options.Enabled,
link: options.URL,
timeout: timeout,
interval: interval,
}
}
func (a *Adapter) SetRemoveEmojis(remove bool) {
a.removeEmojis = remove
}
func (a *Adapter) Start() error {
a.history = service.PtrFromContext[urltest.HistoryStorage](a.ctx)
if a.history == nil {
a.history = urltest.NewHistoryStorage()
}
go a.loopCheck()
return nil
}
func (a *Adapter) Type() string {
return a.providerType
}
func (a *Adapter) Tag() string {
return a.providerTag
}
func (a *Adapter) Outbounds() []adapter.Outbound {
return a.outbounds
}
func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
if a.outboundsByTag == nil {
return nil, false
}
detour, ok := a.outboundsByTag[tag]
return detour, ok
}
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)
outbounds = make([]adapter.Outbound, 0, len(newOpts))
outboundsByTag = make(map[string]adapter.Outbound)
)
for _, opt := range oldOpts {
oldOptByTag[opt.Tag] = opt
}
for i, opt := range newOpts {
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
} else {
tag = F.ToString(a.providerTag, "/", i)
}
outbound, exist := a.outbound.Outbound(tag)
if !exist || !reflect.DeepEqual(opt, oldOptByTag[opt.Tag]) {
err := a.outbound.Create(
adapter.WithContext(a.ctx, &adapter.InboundContext{
Outbound: tag,
}),
a.router,
a.logFactory.NewLogger(F.ToString("outbound/", opt.Type, "[", tag, "]")),
tag,
opt.Type,
opt.Options,
)
if err != nil {
a.logger.Warn(err, " in ", tag, ", skip create this outbound")
continue
}
outbound, _ = a.outbound.Outbound(tag)
}
outbounds = append(outbounds, outbound)
outboundsByTag[tag] = outbound
}
if a.enabled && a.history != nil {
go a.HealthCheck(a.ctx)
}
a.outbounds = outbounds
a.outboundsByTag = outboundsByTag
}
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
if a.ticker != nil {
a.ticker.Reset(a.interval)
}
return a.healthcheck(ctx)
}
func (a *Adapter) RegisterCallback(callback adapter.ProviderUpdateCallback) *list.Element[adapter.ProviderUpdateCallback] {
a.callbackAccess.Lock()
defer a.callbackAccess.Unlock()
return a.callbacks.PushBack(callback)
}
func (a *Adapter) UnregisterCallback(element *list.Element[adapter.ProviderUpdateCallback]) {
a.callbackAccess.Lock()
defer a.callbackAccess.Unlock()
a.callbacks.Remove(element)
}
func (a *Adapter) UpdateGroups() {
for element := a.callbacks.Front(); element != nil; element = element.Next() {
element.Value(a.providerTag)
}
}
func (a *Adapter) Close() error {
if a.ticker != nil {
a.ticker.Stop()
}
outbounds := a.outbounds
a.outbounds = nil
var err error
for _, ob := range outbounds {
if err2 := a.outbound.Remove(ob.Tag()); err2 != nil {
err = E.Append(err, err2, func(err error) error {
return E.Cause(err, "close outbound [", ob.Tag(), "]")
})
}
}
return err
}
func (a *Adapter) loopCheck() {
if !a.enabled {
return
}
a.ticker = time.NewTicker(a.interval)
a.healthcheck(a.ctx)
for {
select {
case <-a.ctx.Done():
return
case <-a.ticker.C:
a.healthcheck(a.ctx)
}
}
}
func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
result := make(map[string]uint16)
if a.checking.Swap(true) {
return result, nil
}
defer a.checking.Store(false)
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
var resultAccess sync.Mutex
checked := make(map[string]bool)
for _, detour := range a.outbounds {
tag := detour.Tag()
if checked[tag] {
continue
}
checked[tag] = true
b.Go(tag, func() (any, error) {
ctx, cancel := context.WithTimeout(a.ctx, a.timeout)
defer cancel()
t, err := urltest.URLTest(ctx, a.link, detour)
if err != nil {
a.logger.Debug("outbound ", tag, " unavailable: ", err)
a.history.DeleteURLTestHistory(tag)
} else {
a.logger.Debug("outbound ", tag, " available: ", t, "ms")
a.history.StoreURLTestHistory(tag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: t,
})
resultAccess.Lock()
result[tag] = t
resultAccess.Unlock()
}
return nil, nil
})
}
b.Wait()
return result, nil
}
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
if len(a.outbounds) == 0 {
return
}
exists := make(map[string]bool)
for i, opt := range newOpts {
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
} else {
tag = F.ToString(a.providerTag, "/", i)
}
exists[tag] = true
}
for _, opt := range a.outbounds {
if !exists[opt.Tag()] {
if err := a.outbound.Remove(opt.Tag()); err != nil {
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
}
}
}
}
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,}`)
-45
View File
@@ -1,45 +0,0 @@
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)
}
}
}
-157
View File
@@ -1,157 +0,0 @@
package provider
import (
"context"
"io"
"os"
"sync"
"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"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.ProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ProviderRegistry
access sync.Mutex
started bool
stage adapter.StartStage
providers []adapter.Provider
providerByTag map[string]adapter.Provider
wg sync.WaitGroup
}
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.Provider),
}
}
func (m *Manager) Initialize() {
}
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 {
err := adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
providers := m.providers
m.providers = nil
m.access.Unlock()
var err error
for _, provider := range providers {
if closer, isCloser := provider.(io.Closer); isCloser {
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
})
monitor.Finish()
}
}
return nil
}
func (m *Manager) Providers() []adapter.Provider {
m.access.Lock()
defer m.access.Unlock()
return m.providers
}
func (m *Manager) Get(tag string) (adapter.Provider, 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.Provider) bool {
return it == provider
})
if index == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return common.Close(provider)
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = common.Close(existsProvider)
if err != nil {
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
return nil
}
-72
View File
@@ -1,72 +0,0 @@
package provider
import (
"context"
"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, router adapter.Router, logFactory log.Factory, tag string, options T) (adapter.Provider, error)
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
registry.register(providerType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, rawOptions any) (adapter.Provider, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logFactory, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ProviderRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options any) (adapter.Provider, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(providerType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[providerType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateProvider(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) (adapter.Provider, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[providerType]
if !loaded {
return nil, E.New("provider type not found: '" + providerType + "'")
}
return constructor(ctx, router, logFactory, tag, options)
}
func (r *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[providerType] = optionsConstructor
r.constructors[providerType] = constructor
}
+106 -98
View File
@@ -3,108 +3,73 @@ package adapter
import (
"context"
"net"
"net/http"
"net/netip"
"time"
"sync"
"github.com/sagernet/sing-box/common/geoip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/control"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
mdns "github.com/miekg/dns"
"go4.org/netipx"
)
type Router interface {
Lifecycle
Service
PreStarter
PostStarter
Cleanup() error
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) (Outbound, error)
FakeIPStore() FakeIPStore
ConnectionRouter
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
PreMatch(metadata InboundContext) error
ConnectionRouterEx
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
ClearDNSCache()
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
WIFIState() WIFIState
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}
type PreMatchAction uint8
ClashServer() ClashServer
SetClashServer(server ClashServer)
const (
PreMatchContinue PreMatchAction = iota
PreMatchFlow
PreMatchReject
PreMatchDrop
PreMatchBypass
PreMatchHijackDNS
)
V2RayServer() V2RayServer
SetV2RayServer(server V2RayServer)
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
ResetNetwork() error
}
// Deprecated: Use ConnectionRouterEx instead.
@@ -119,9 +84,18 @@ type ConnectionRouterEx interface {
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
func ContextWithRouter(ctx context.Context, router Router) context.Context {
return service.ContextWith(ctx, router)
}
func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx)
}
type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
@@ -135,20 +109,54 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
type DNSRuleSetUpdateValidator interface {
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
access sync.Mutex
httpClientCache map[string]*http.Client
}
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
type RuleSetMetadata struct {
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
func NewHTTPStartContext() *HTTPStartContext {
return &HTTPStartContext{
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))
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *HTTPStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
}
type InterfaceUpdateListener interface {
InterfaceUpdated()
}
type WIFIState struct {
SSID string
BSSID string
}
+4 -10
View File
@@ -2,8 +2,6 @@ package adapter
import (
C "github.com/sagernet/sing-box/constant"
"github.com/miekg/dns"
)
type HeadlessRule interface {
@@ -13,20 +11,16 @@ type HeadlessRule interface {
type Rule interface {
HeadlessRule
SimpleLifecycle
Service
Type() string
UpdateGeosite() error
Action() RuleAction
}
type DNSRule interface {
Rule
LegacyPreMatch(metadata *InboundContext) bool
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
MatchResponseTag() string
MatchResponseTags() []string
MatchResponseAnonymous() bool
Race() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleAction interface {
@@ -36,7 +30,7 @@ type RuleAction interface {
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
return false
default:
return true
-19
View File
@@ -1,19 +0,0 @@
package adapter
import (
"context"
"github.com/sagernet/sing/service"
)
type SecurityPolicy interface {
CheckFeature(ctx context.Context, feature string) error
}
func CheckSecurityFeature(ctx context.Context, feature string) error {
policy := service.FromContext[SecurityPolicy](ctx)
if policy == nil {
return nil
}
return policy.CheckFeature(ctx, feature)
}
+2 -23
View File
@@ -1,27 +1,6 @@
package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface {
Lifecycle
Type() string
Tag() string
}
type ServiceRegistry interface {
option.ServiceOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
}
type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
Start() error
Close() error
}
-21
View File
@@ -1,21 +0,0 @@
package service
type Adapter struct {
serviceType string
serviceTag string
}
func NewAdapter(serviceType string, serviceTag string) Adapter {
return Adapter{
serviceType: serviceType,
serviceTag: serviceTag,
}
}
func (a *Adapter) Type() string {
return a.serviceType
}
func (a *Adapter) Tag() string {
return a.serviceTag
}
-153
View File
@@ -1,153 +0,0 @@
package service
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
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
services := m.services
m.access.Unlock()
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(service, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
return m.services
}
func (m *Manager) Get(tag string) (adapter.Service, bool) {
m.access.Lock()
service, found := m.serviceByTag[tag]
m.access.Unlock()
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(service, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
return nil
}
-80
View File
@@ -1,80 +0,0 @@
package service
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.Service, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ServiceRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, 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(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
}
-18
View File
@@ -1,18 +0,0 @@
package adapter
import (
"net"
N "github.com/sagernet/sing/common/network"
)
type ManagedSSMServer interface {
Inbound
SetTracker(tracker SSMTracker)
UpdateUsers(users []string, uPSKs []string) error
}
type SSMTracker interface {
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
}
-104
View File
@@ -1,104 +0,0 @@
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
}
+1 -1
View File
@@ -3,6 +3,6 @@ package adapter
import "time"
type TimeService interface {
SimpleLifecycle
Service
TimeFunc() func() time.Time
}
+42 -42
View File
@@ -9,31 +9,31 @@ import (
)
type (
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)
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)
)
func NewUpstreamHandler(
func NewUpstreamHandlerEx(
metadata InboundContext,
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
connectionHandler ConnectionHandlerFuncEx,
packetHandler PacketConnectionHandlerFuncEx,
) UpstreamHandlerAdapterEx {
return &myUpstreamHandlerWrapperEx{
metadata: metadata,
connectionHandler: connectionHandler,
packetHandler: packetHandler,
}
}
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
type myUpstreamHandlerWrapper struct {
type myUpstreamHandlerWrapperEx struct {
metadata InboundContext
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFuncEx
}
func (w *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (w *myUpstreamHandlerWrapperEx) 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 *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net
w.connectionHandler(ctx, conn, myMetadata, onClose)
}
func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (w *myUpstreamHandlerWrapperEx) 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,25 +55,25 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, co
w.packetHandler(ctx, conn, myMetadata, onClose)
}
var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
type myUpstreamContextHandlerWrapperEx struct {
connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFuncEx
}
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
func NewUpstreamContextHandlerEx(
connectionHandler ConnectionHandlerFuncEx,
packetHandler PacketConnectionHandlerFuncEx,
) UpstreamHandlerAdapterEx {
return &myUpstreamContextHandlerWrapperEx{
connectionHandler: connectionHandler,
packetHandler: packetHandler,
}
}
func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, myMetadata := ExtendContext(ctx)
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx)
if source.IsValid() {
myMetadata.Source = source
}
@@ -83,8 +83,8 @@ func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, c
w.connectionHandler(ctx, conn, *myMetadata, onClose)
}
func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, myMetadata := ExtendContext(ctx)
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx)
if source.IsValid() {
myMetadata.Source = source
}
@@ -94,24 +94,24 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Cont
w.packetHandler(ctx, conn, *myMetadata, onClose)
}
func NewRouteHandler(
func NewRouteHandlerEx(
metadata InboundContext,
router ConnectionRouterEx,
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
) UpstreamHandlerAdapterEx {
return &routeHandlerWrapperEx{
metadata: metadata,
router: router,
}
}
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
type routeHandlerWrapper struct {
type routeHandlerWrapperEx struct {
metadata InboundContext
router ConnectionRouterEx
}
func (r *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (r *routeHandlerWrapperEx) 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 *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
}
func (r *routeHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
@@ -131,22 +131,22 @@ func (r *routeHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
}
func NewRouteContextHandler(
func NewRouteContextHandlerEx(
router ConnectionRouterEx,
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
) UpstreamHandlerAdapterEx {
return &routeContextHandlerWrapperEx{
router: router,
}
}
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
type routeContextHandlerWrapper struct {
type routeContextHandlerWrapperEx struct {
router ConnectionRouterEx
}
func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, metadata := ExtendContext(ctx)
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
if source.IsValid() {
metadata.Source = source
}
@@ -156,8 +156,8 @@ func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn n
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
}
func (r *routeContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
_, metadata := ExtendContext(ctx)
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
if source.IsValid() {
metadata.Source = source
}
+47 -74
View File
@@ -12,30 +12,19 @@ import (
type (
// Deprecated
LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
// Deprecated
LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
)
// Deprecated
//
//nolint:staticcheck
type LegacyUpstreamHandlerAdapter interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
E.Handler
}
// Deprecated
//
//nolint:staticcheck
func NewLegacyUpstreamHandler(
func NewUpstreamHandler(
metadata InboundContext,
connectionHandler LegacyConnectionHandlerFunc,
packetHandler LegacyPacketConnectionHandlerFunc,
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) LegacyUpstreamHandlerAdapter {
return &legacyUpstreamHandlerWrapper{
) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
metadata: metadata,
connectionHandler: connectionHandler,
packetHandler: packetHandler,
@@ -43,20 +32,17 @@ func NewLegacyUpstreamHandler(
}
}
var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil)
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
// Deprecated: use NewUpstreamHandler instead.
//
//nolint:staticcheck
type legacyUpstreamHandlerWrapper struct {
// Deprecated
type myUpstreamHandlerWrapper struct {
metadata InboundContext
connectionHandler LegacyConnectionHandlerFunc
packetHandler LegacyPacketConnectionHandlerFunc
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
// Deprecated: use NewUpstreamHandler instead.
func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -67,8 +53,7 @@ func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn n
return w.connectionHandler(ctx, conn, myMetadata)
}
// Deprecated: use NewUpstreamHandler instead.
func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -79,41 +64,39 @@ func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context,
return w.packetHandler(ctx, conn, myMetadata)
}
// Deprecated: use NewUpstreamHandler instead.
func (w *legacyUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated: removed
// Deprecated
func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{
Source: metadata.Source.Unwrap(),
Destination: metadata.Destination.Unwrap(),
Source: metadata.Source,
Destination: metadata.Destination,
}
}
// Deprecated: Use NewUpstreamContextHandler instead.
type legacyUpstreamContextHandlerWrapper struct {
connectionHandler LegacyConnectionHandlerFunc
packetHandler LegacyPacketConnectionHandlerFunc
// Deprecated
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
// Deprecated: Use NewUpstreamContextHandler instead.
func NewLegacyUpstreamContextHandler(
connectionHandler LegacyConnectionHandlerFunc,
packetHandler LegacyPacketConnectionHandlerFunc,
// Deprecated
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) LegacyUpstreamHandlerAdapter {
return &legacyUpstreamContextHandlerWrapper{
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
connectionHandler: connectionHandler,
packetHandler: packetHandler,
errorHandler: errorHandler,
}
}
// Deprecated: Use NewUpstreamContextHandler instead.
func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -124,8 +107,7 @@ func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context,
return w.connectionHandler(ctx, conn, *myMetadata)
}
// Deprecated: Use NewUpstreamContextHandler instead.
func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -136,18 +118,17 @@ func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Co
return w.packetHandler(ctx, conn, *myMetadata)
}
// Deprecated: Use NewUpstreamContextHandler instead.
func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated: Use ConnectionRouterEx instead.
func NewLegacyRouteHandler(
func NewRouteHandler(
metadata InboundContext,
router ConnectionRouter,
logger logger.ContextLogger,
) LegacyUpstreamHandlerAdapter {
return &legacyRouteHandlerWrapper{
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
metadata: metadata,
router: router,
logger: logger,
@@ -155,29 +136,26 @@ func NewLegacyRouteHandler(
}
// Deprecated: Use ConnectionRouterEx instead.
func NewLegacyRouteContextHandler(
func NewRouteContextHandler(
router ConnectionRouter,
logger logger.ContextLogger,
) LegacyUpstreamHandlerAdapter {
return &legacyRouteContextHandlerWrapper{
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
router: router,
logger: logger,
}
}
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil)
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
//
//nolint:staticcheck
type legacyRouteHandlerWrapper struct {
type routeHandlerWrapper struct {
metadata InboundContext
router ConnectionRouter
logger logger.ContextLogger
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -188,8 +166,7 @@ func (w *legacyRouteHandlerWrapper) NewConnection(ctx context.Context, conn net.
return w.router.RouteConnection(ctx, conn, myMetadata)
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -200,21 +177,19 @@ func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, con
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) {
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil)
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
type legacyRouteContextHandlerWrapper struct {
type routeContextHandlerWrapper struct {
router ConnectionRouter
logger logger.ContextLogger
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -225,8 +200,7 @@ func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, co
return w.router.RouteConnection(ctx, conn, *myMetadata)
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -237,7 +211,6 @@ func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Conte
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) {
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
-15
View File
@@ -1,15 +0,0 @@
//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
@@ -1,7 +0,0 @@
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
package adapter
type USBIPDynamicServer interface {
usbipNotIncluded()
}
+212 -481
View File
@@ -4,31 +4,16 @@ 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"
"github.com/sagernet/sing-box/adapter/provider"
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/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
@@ -36,53 +21,32 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
var _ adapter.SimpleLifecycle = (*Box)(nil)
var _ adapter.Service = (*Box)(nil)
type Box 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{}
createdAt time.Time
router adapter.Router
inbounds []adapter.Inbound
outbounds []adapter.Outbound
logFactory log.Factory
logger log.ContextLogger
preServices1 map[string]adapter.Service
preServices2 map[string]adapter.Service
postServices map[string]adapter.Service
done chan struct{}
}
type Options struct {
option.Options
Context context.Context
PlatformLogWriter log.PlatformWriter
NetworkNamespaceHolderArgs []string
Context context.Context
PlatformLogWriter log.PlatformWriter
}
func Context(
ctx context.Context,
inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
providerRegistry adapter.ProviderRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
certificateProviderRegistry adapter.CertificateProviderRegistry,
) context.Context {
func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
@@ -93,28 +57,6 @@ func Context(
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
}
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
}
if service.FromContext[option.ProviderOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.ProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.ProviderOptionsRegistry](ctx, providerRegistry)
ctx = service.ContextWith[adapter.ProviderRegistry](ctx, providerRegistry)
}
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
}
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
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
}
@@ -124,45 +66,18 @@ func New(options Options) (*Box, error) {
if ctx == nil {
ctx = context.Background()
}
ctx = service.ContextWithDefaultRegistry(ctx)
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
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")
}
if inboundRegistry == nil {
return nil, E.New("missing inbound registry in context")
}
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context")
}
if providerRegistry == nil {
return nil, E.New("missing provider registry in context")
}
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
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 = service.ContextWithDefaultRegistry(ctx)
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
debugOptions := common.PtrValueOrDefault(experimentalOptions.Debug)
err := checkDebugOptions(debugOptions)
if err != nil {
return nil, err
}
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needCacheFile bool
var needClashAPI bool
var needV2RayAPI bool
@@ -175,16 +90,7 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
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)
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer
if platformInterface != nil {
defaultLogWriter = io.Discard
@@ -192,7 +98,7 @@ func New(options Options) (*Box, error) {
logFactory, err := log.New(log.Options{
Context: ctx,
Options: common.PtrValueOrDefault(options.Log),
Observable: needClashAPI || needAPIService,
Observable: needClashAPI,
DefaultWriter: defaultLogWriter,
BaseTime: createdAt,
PlatformWriter: options.PlatformLogWriter,
@@ -200,134 +106,48 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "create log factory")
}
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 ||
len(certificateOptions.CertificatePath) > 0 ||
len(certificateOptions.CertificateDirectoryPath) > 0 {
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
if err != nil {
return nil, err
}
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
internalServices = append(internalServices, certificateStore)
}
netnsManager, err := netns.NewManager(logFactory.NewLogger("netns"), options.NetworkNamespaces, options.NetworkNamespaceHolderArgs)
router, err := route.NewRouter(
ctx,
logFactory,
common.PtrValueOrDefault(options.Route),
common.PtrValueOrDefault(options.DNS),
common.PtrValueOrDefault(options.NTP),
options.Inbounds,
)
if err != nil {
return nil, err
return nil, E.Cause(err, "parse route options")
}
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)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
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)
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")
}
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
// 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 {
timeService = new(tls.TimeServiceWrapper)
service.MustRegister[ntp.TimeService](ctx, timeService)
}
for i, transportOptions := range dnsOptions.Servers {
var tag string
if transportOptions.Tag != "" {
tag = transportOptions.Tag
} else {
tag = F.ToString(i)
}
err = dnsTransportManager.Create(
ctx,
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
tag,
transportOptions.Type,
transportOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize DNS server[", i, "]")
}
}
err = dnsRouter.Initialize(dnsOptions.Rules)
if err != nil {
return nil, E.Cause(err, "initialize dns router")
}
for i, endpointOptions := range options.Endpoints {
var tag string
if endpointOptions.Tag != "" {
tag = endpointOptions.Tag
} else {
tag = F.ToString(i)
}
endpointCtx := ctx
if tag != "" {
// TODO: remove this
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
Outbound: tag,
//nolint:staticcheck
if len(options.LegacyInbounds) > 0 {
for _, legacyInbound := range options.LegacyInbounds {
options.Inbounds = append(options.Inbounds, option.Inbound{
Type: legacyInbound.Type,
Tag: legacyInbound.Tag,
Options: common.Must1(legacyInbound.RawOptions()),
})
}
err = endpointManager.Create(
endpointCtx,
router,
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
tag,
endpointOptions.Type,
endpointOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize endpoint[", i, "]")
}
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
//nolint:staticcheck
if len(options.LegacyOutbounds) > 0 {
for _, legacyOutbound := range options.LegacyOutbounds {
options.Outbounds = append(options.Outbounds, option.Outbound{
Type: legacyOutbound.Type,
Tag: legacyOutbound.Tag,
Options: common.Must1(legacyOutbound.RawOptions()),
})
}
}
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
for i, inboundOptions := range options.Inbounds {
var currentInbound adapter.Inbound
var tag string
if inboundOptions.Tag != "" {
tag = inboundOptions.Tag
} else {
tag = F.ToString(i)
}
err = inboundManager.Create(
currentInbound, err = inboundRegistry.CreateInbound(
ctx,
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
@@ -336,32 +156,12 @@ func New(options Options) (*Box, error) {
inboundOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize inbound[", i, "]")
}
}
options.Outbounds = append(options.Outbounds, option.Outbound{
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, "]")
return nil, E.Cause(err, "parse inbound[", i, "]")
}
inbounds = append(inbounds, currentInbound)
}
for i, outboundOptions := range options.Outbounds {
var currentOutbound adapter.Outbound
var tag string
if outboundOptions.Tag != "" {
tag = outboundOptions.Tag
@@ -375,7 +175,7 @@ func New(options Options) (*Box, error) {
Outbound: tag,
})
}
err = outboundManager.Create(
currentOutbound, err = outboundRegistry.CreateOutbound(
outboundCtx,
router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
@@ -384,144 +184,65 @@ func New(options Options) (*Box, error) {
outboundOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize outbound[", i, "]")
return nil, E.Cause(err, "parse outbound[", i, "]")
}
outbounds = append(outbounds, currentOutbound)
}
for i, providerOptions := range options.Providers {
var tag string
if providerOptions.Tag != "" {
tag = providerOptions.Tag
} else {
tag = F.ToString(i)
}
err = providerManager.Create(
ctx,
router,
logFactory,
tag,
providerOptions.Type,
providerOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize provider[", i, "]")
}
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{})
common.Must(cErr)
outbounds = append(outbounds, defaultOutbound)
return defaultOutbound
})
if err != nil {
return nil, err
}
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) {
return direct.NewOutbound(
ctx,
router,
logFactory.NewLogger("outbound/direct"),
"direct",
option.DirectOutboundOptions{},
)
})
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
return dnsTransportRegistry.CreateDNSTransport(
ctx,
logFactory.NewLogger("dns/local"),
"local",
C.DNSTypeLocal,
&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)
err = platformInterface.Initialize(ctx, router)
if err != nil {
return nil, E.Cause(err, "initialize platform interface")
}
}
preServices1 := make(map[string]adapter.Service)
preServices2 := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needCacheFile {
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
internalServices = append(internalServices, cacheFile)
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
}
preServices1["cache file"] = cacheFile
}
if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
if err != nil {
return nil, E.Cause(err, "create clash-server")
return nil, E.Cause(err, "create clash api server")
}
service.MustRegister[adapter.ClashServer](ctx, clashServer)
internalServices = append(internalServices, clashServer)
router.SetClashServer(clashServer)
preServices2["clash api"] = clashServer
}
if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
if err != nil {
return nil, E.Cause(err, "create v2ray-server")
return nil, E.Cause(err, "create v2ray api server")
}
if v2rayServer.StatsService() != nil {
router.AppendTracker(v2rayServer.StatsService())
internalServices = append(internalServices, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
}
}
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")
}
ntpService := ntp.NewService(ntp.Options{
Context: ctx,
Dialer: ntpDialer,
Logger: logFactory.NewLogger("ntp"),
Server: ntpOptions.ServerOptions.Build(),
Interval: time.Duration(ntpOptions.Interval),
WriteToSystem: ntpOptions.WriteToSystem,
})
timeService.TimeService = ntpService
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
router.SetV2RayServer(v2rayServer)
preServices2["v2ray api"] = v2rayServer
}
return &Box{
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{}),
router: router,
inbounds: inbounds,
outbounds: outbounds,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
preServices1: preServices1,
preServices2: preServices2,
postServices: postServices,
done: make(chan struct{}),
}, nil
}
@@ -540,7 +261,7 @@ func (s *Box) PreStart() error {
s.Close()
return err
}
s.logger.Notice("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
return nil
}
@@ -559,7 +280,7 @@ func (s *Box) Start() error {
s.Close()
return err
}
s.logger.Notice("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
return nil
}
@@ -571,32 +292,35 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
applyDebugOptions(s.debugOptions)
s.debugHTTPServer, err = startDebugHTTPServer(s.debugOptions)
for serviceName, service := range s.preServices1 {
if preService, isPreService := service.(adapter.PreStarter); isPreService {
monitor.Start("pre-start ", serviceName)
err := preService.PreStart()
monitor.Finish()
if err != nil {
return E.Cause(err, "pre-start ", serviceName)
}
}
}
for serviceName, service := range s.preServices2 {
if preService, isPreService := service.(adapter.PreStarter); isPreService {
monitor.Start("pre-start ", serviceName)
err := preService.PreStart()
monitor.Finish()
if err != nil {
return E.Cause(err, "pre-start ", serviceName)
}
}
}
err = s.router.PreStart()
if err != nil {
return E.Cause(err, "pre-start router")
}
err = s.startOutbounds()
if err != nil {
return err
}
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.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
}
return nil
return s.router.Start()
}
func (s *Box) start() error {
@@ -604,37 +328,64 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, s.internalService)
for serviceName, service := range s.preServices1 {
err = service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
for serviceName, service := range s.preServices2 {
err = service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
for i, in := range s.inbounds {
var tag string
if in.Tag() == "" {
tag = F.ToString(i)
} else {
tag = in.Tag()
}
err = in.Start()
if err != nil {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
}
}
err = s.postStart()
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.endpoint)
return s.router.Cleanup()
}
func (s *Box) postStart() error {
for serviceName, service := range s.postServices {
err := service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
}
}
// TODO: reorganize ALL start order
for _, out := range s.outbounds {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart()
if err != nil {
return E.Cause(err, "post-start outbound/", out.Tag())
}
}
}
err := s.router.PostStart()
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.inbound, s.service)
if err != nil {
return err
}
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.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
for _, in := range s.inbounds {
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
err = lateInbound.PostStart()
if err != nil {
return E.Cause(err, "post-start inbound/", in.Tag())
}
}
}
return nil
}
@@ -646,78 +397,58 @@ func (s *Box) Close() error {
default:
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")
monitor := taskmonitor.New(s.logger, C.StopTimeout)
var errors error
for serviceName, service := range s.postServices {
monitor.Start("close ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName)
})
s.debugHTTPServer = nil
monitor.Finish()
}
for _, closeItem := range []struct {
name string
service adapter.Lifecycle
}{
{"service", s.service},
{"inbound", s.inbound},
{"certificate-provider", s.certificateProvider},
{"provider", s.provider},
{"endpoint", s.endpoint},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
{"dns-router", s.dnsRouter},
{"dns-transport", s.dnsTransport},
{"network", s.network},
} {
done := adapter.LogElapsed(s.logger, "close ", closeItem.name)
err = E.Append(err, closeItem.service.Close(), func(err error) error {
return E.Cause(err, "close ", closeItem.name)
for i, in := range s.inbounds {
monitor.Start("close inbound/", in.Type(), "[", i, "]")
errors = E.Append(errors, in.Close(), func(err error) error {
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
})
done()
monitor.Finish()
}
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())
for i, out := range s.outbounds {
monitor.Start("close outbound/", out.Type(), "[", i, "]")
errors = E.Append(errors, common.Close(out), func(err error) error {
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
})
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
monitor.Finish()
}
for _, lifecycleService := range s.internalService {
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name())
monitor.Start("close router")
if err := common.Close(s.router); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close router")
})
done()
}
done := adapter.LogElapsed(s.logger, "close logger")
err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger")
})
done()
return err
}
func (s *Box) Network() adapter.NetworkManager {
return s.network
monitor.Finish()
for serviceName, service := range s.preServices1 {
monitor.Start("close ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName)
})
monitor.Finish()
}
for serviceName, service := range s.preServices2 {
monitor.Start("close ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
return E.Cause(err, "close ", serviceName)
})
monitor.Finish()
}
if err := common.Close(s.logFactory); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close logger")
})
}
return errors
}
func (s *Box) Router() adapter.Router {
return s.router
}
func (s *Box) Inbound() adapter.InboundManager {
return s.inbound
}
func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}
func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}
+85
View File
@@ -0,0 +1,85 @@
package box
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
func (s *Box) startOutbounds() error {
monitor := taskmonitor.New(s.logger, C.StartTimeout)
outboundTags := make(map[adapter.Outbound]string)
outbounds := make(map[string]adapter.Outbound)
for i, outboundToStart := range s.outbounds {
var outboundTag string
if outboundToStart.Tag() == "" {
outboundTag = F.ToString(i)
} else {
outboundTag = outboundToStart.Tag()
}
if _, exists := outbounds[outboundTag]; exists {
return E.New("outbound tag ", outboundTag, " duplicated")
}
outboundTags[outboundToStart] = outboundTag
outbounds[outboundTag] = outboundToStart
}
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, outboundToStart := range s.outbounds {
outboundTag := outboundTags[outboundToStart]
if started[outboundTag] {
continue
}
dependencies := outboundToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
}
}
if len(started) == len(s.outbounds) {
break
}
if canContinue {
continue
}
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
return !started[outboundTags[it]]
})
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
problemOutbound := outbounds[problemOutboundTag]
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
}
return nil
}
Submodule clients/desktop deleted from cebee0d527
-194
View File
@@ -1,194 +0,0 @@
// Command admin_panel_pack post-processes a directory of built SPA
// assets so it can be served straight from the Go binary via //go:embed.
// It does *not* generate any Go source any more — that responsibility
// moved to the embed directive in service/admin_panel/service.go.
//
// Three transformations happen here, all in-place inside the supplied
// directory:
//
// 1. Legacy WOFF (1.0) fallback fonts are deleted. Every browser made
// after 2014 reads WOFF2 natively, so shipping both formats roughly
// doubles the embedded font payload for no real-world benefit. The
// matching `,url(*.woff) format("woff")` segments are stripped from
// the bundled CSS in step (2) so the @font-face rules don't reference
// files that aren't shipped.
// 2. Bundled CSS is rewritten to drop those WOFF URL fragments.
// 3. Compressible text assets (.html, .css, .js, .svg, .json, .map) are
// pre-gzipped as companion `*.gz` files. The HTTP handler then either
// passes those bytes through verbatim with Content-Encoding: gzip or
// falls back to the raw file for the rare client that does not
// advertise gzip support — no on-line compression, no surprises.
// Already-compressed formats (.woff2, fonts, images) are skipped: gzip
// can't shrink them and the duplicate would only inflate the binary.
package main
import (
"bytes"
"compress/gzip"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
func main() {
dir := flag.String("dir", "service/admin_panel/dist", "directory of built SPA assets to post-process in place")
flag.Parse()
woffDropped, err := pruneWoff(*dir)
if err != nil {
fail("prune woff: %v", err)
}
cssRewritten, err := rewriteCSS(*dir)
if err != nil {
fail("rewrite css: %v", err)
}
stats, err := gzipText(*dir)
if err != nil {
fail("gzip text: %v", err)
}
fmt.Fprintf(os.Stderr, "post-processed %s: dropped %d woff, rewrote %d css, gzipped %d files (%d→%d bytes)\n",
*dir, woffDropped, cssRewritten, stats.gzipped, stats.totalRaw, stats.totalGz)
}
// pruneWoff deletes every legacy *.woff (WOFF 1.0) font under dir. The
// bundled CSS still references them on entry; rewriteCSS drops those
// references in a separate pass so the two operations stay independently
// testable.
func pruneWoff(dir string) (int, error) {
var n int
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() || !strings.EqualFold(filepath.Ext(p), ".woff") {
return nil
}
if err := os.Remove(p); err != nil {
return err
}
n++
return nil
})
return n, err
}
// woffRefAfterRE / woffRefBeforeRE match a single WOFF 1.0 entry inside a
// Vite-bundled CSS `src:` declaration. Vite minifies the rule to
// `src:url(./X.woff2) format("woff2"),url(./X.woff) format("woff");` so the
// "after" regex (the common case) eats the *leading* comma + woff entry,
// leaving only the woff2 source. We also handle the rare reverse ordering
// in a second pass.
var (
woffRefAfterRE = regexp.MustCompile(`,\s*url\([^)]*\.woff\)\s*format\(["']woff["']\)`)
woffRefBeforeRE = regexp.MustCompile(`url\([^)]*\.woff\)\s*format\(["']woff["']\)\s*,\s*`)
)
// rewriteCSS drops every reference to a *.woff URL from every *.css file
// under dir. Pairs naturally with pruneWoff: after both passes, no font
// URL in the bundle points at a file that isn't shipped.
func rewriteCSS(dir string) (int, error) {
var n int
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() || !strings.EqualFold(filepath.Ext(p), ".css") {
return nil
}
data, err := os.ReadFile(p)
if err != nil {
return err
}
out := woffRefAfterRE.ReplaceAll(data, nil)
out = woffRefBeforeRE.ReplaceAll(out, nil)
if bytes.Equal(out, data) {
return nil
}
if err := os.WriteFile(p, out, 0o644); err != nil {
return err
}
n++
return nil
})
return n, err
}
// gzipExts is the set of file extensions for which a `.gz` companion is
// generated. Anything not on this list is left alone — woff2/png/jpeg/etc.
// are already compressed, so gzip can only inflate them slightly while
// doubling the embedded payload.
var gzipExts = map[string]bool{
".html": true,
".css": true,
".js": true,
".mjs": true,
".svg": true,
".json": true,
".map": true,
".txt": true,
".xml": true,
".wasm": true,
}
type gzipStats struct {
gzipped int
totalRaw int64
totalGz int64
}
// gzipText produces a `<file>.gz` companion next to every text-like asset
// in dir, using gzip.BestCompression. The companion is dropped if the
// compressed bytes don't save at least 10 % over the raw file — same
// heuristic we used in the previous (Go-source-emitting) generation, just
// applied to disk files now.
func gzipText(dir string) (gzipStats, error) {
var stats gzipStats
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(p))
if ext == ".gz" || !gzipExts[ext] {
return nil
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
var buf bytes.Buffer
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return err
}
// Reproducible: no mtime, no OS marker.
w.ModTime = time.Time{}
w.OS = 0xff
if _, err := w.Write(raw); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
if buf.Len() > len(raw)*9/10 {
return nil
}
stats.gzipped++
stats.totalRaw += int64(len(raw))
stats.totalGz += int64(buf.Len())
return os.WriteFile(p+".gz", buf.Bytes(), 0o644)
})
return stats, err
}
func fail(format string, args ...any) {
fmt.Fprintf(os.Stderr, "admin_panel_pack: "+format+"\n", args...)
os.Exit(1)
}
-468
View File
@@ -1,468 +0,0 @@
package main
import (
"context"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/sagernet/asc-go/asc"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"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"
)
func main() {
ctx := context.Background()
switch os.Args[1] {
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)
}
case "publish_testflight":
err := publishTestflight(ctx)
if err != nil {
log.Fatal(err)
}
case "cancel_app_store":
err := cancelAppStore(ctx, os.Args[2])
if err != nil {
log.Fatal(err)
}
case "prepare_app_store":
err := prepareAppStore(ctx)
if err != nil {
log.Fatal(err)
}
case "publish_app_store":
err := publishAppStore(ctx)
if err != nil {
log.Fatal(err)
}
default:
log.Fatal("unknown action: ", os.Args[1])
}
}
const (
appID = "6785326793"
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
)
func createClient(expireDuration time.Duration) *asc.Client {
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
if err != nil {
log.Fatal(err)
}
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
if err != nil {
log.Fatal(err)
}
return asc.NewClient(tokenConfig.Client())
}
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)
builds, _, err := client.Builds.ListBuilds(ctx, query)
if err != nil {
return err
}
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
}
}
os.Stdout.WriteString(F.ToString(nextProjectVersion, "\n"))
return nil
}
func publishTestflight(ctx context.Context) error {
if len(os.Args) < 3 {
return E.New("platform required: ios, macos, or tvos")
}
var platform asc.Platform
switch os.Args[2] {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", os.Args[2])
}
tagVersion, err := build_shared.ReadTagVersion()
if err != nil {
return err
}
tag := tagVersion.VersionString()
releaseNotes := F.ToString("sing-box ", tagVersion.String())
if len(os.Args) >= 4 {
releaseNotes = strings.Join(os.Args[3:], " ")
}
client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
if err != nil {
return err
}
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
waitingForProcess := false
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
build := builds.Data[0]
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Fatal(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(releaseNotes))
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
}
return err
}
}
break
}
return nil
}
func cancelAppStore(ctx context.Context, platform string) error {
switch platform {
case "ios":
platform = string(asc.PlatformIOS)
case "macos":
platform = string(asc.PlatformMACOS)
case "tvos":
platform = string(asc.PlatformTVOS)
}
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for {
log.Info(platform, " list versions")
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if isRetryable(response) {
continue
} else if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
if version.ID == "" {
return nil
}
log.Info(platform, " ", tag, " get submission")
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
if isRetryable(response) {
continue
} else if err != nil {
return err
}
log.Info(platform, " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
return nil
}
}
func prepareAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
log.Info(string(platform), " ", tag, " list builds")
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Fatal(string(platform), " ", tag, " no build found")
}
buildID := common.Ptr(builds.Data[0].ID)
if version.ID == "" {
log.Info(string(platform), " ", tag, " create version")
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
Platform: platform,
VersionString: tag,
}, appID, buildID)
if err != nil {
return err
}
version = newVersion.Data
} else {
log.Info(string(platform), " ", tag, " check build")
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
if submission != nil {
log.Info(string(platform), " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
time.Sleep(5 * time.Second)
}
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
log.Info(string(platform), " ", tag, " update build")
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
if err != nil {
return err
}
if response.StatusCode != http.StatusNoContent {
response.Write(os.Stderr)
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
}
} else {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
continue
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
}
}
log.Info(string(platform), " ", tag, " list localization")
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Info(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
})
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " create submission")
fixSubmit:
for {
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
if err != nil {
switch response.StatusCode {
case http.StatusInternalServerError:
continue
default:
return err
}
}
switch response.StatusCode {
case http.StatusCreated:
break fixSubmit
default:
return err
}
}
}
return nil
}
func publishAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
log.Fatal(string(platform), " ", tag, " not submitted")
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview:
log.Warn(string(platform), " ", tag, " waiting for review")
continue
case asc.AppStoreVersionStatePendingDeveloperRelease:
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
if err != nil {
return err
}
}
return nil
}
func isRetryable(response *asc.Response) bool {
if response == nil {
return false
}
switch response.StatusCode {
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
return true
default:
return false
}
}
-236
View File
@@ -1,236 +0,0 @@
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
}
+27 -131
View File
@@ -5,29 +5,22 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
_ "github.com/sagernet/gomobile"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/shell"
)
var (
debugEnabled bool
target string
platform string
// withTailscale bool
)
func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
// flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
}
func main() {
@@ -38,8 +31,8 @@ func main() {
switch target {
case "android":
buildAndroid()
case "apple":
buildApple()
case "ios":
buildiOS()
}
}
@@ -47,9 +40,7 @@ var (
sharedFlags []string
debugFlags []string
sharedTags []string
darwinTags []string
// memcTags []string
notMemcTags []string
iosTags []string
debugTags []string
)
@@ -60,82 +51,36 @@ func init() {
if err != nil {
currentTag = "unknown"
}
sharedFlags = append(sharedFlags, "-ldflags", build_shared.LinkerFlags(currentTag, false))
debugFlags = append(debugFlags, "-ldflags", build_shared.LinkerFlags(currentTag, true))
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
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")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
debugTags = append(debugTags, "debug")
}
type AndroidBuildConfig struct {
AndroidAPI int
OutputName string
Tags []string
}
func buildAndroid() {
build_shared.FindSDK()
func filterTags(tags []string, exclude ...string) []string {
excludeMap := make(map[string]bool)
for _, tag := range exclude {
excludeMap[tag] = true
}
var result []string
for _, tag := range tags {
if !excludeMap[tag] {
result = append(result, tag)
}
}
return result
}
func checkJavaVersion() {
var javaPath string
javaHome := os.Getenv("JAVA_HOME")
if javaHome == "" {
javaPath = "java"
} else {
javaPath = filepath.Join(javaHome, "bin", "java")
}
javaVersion, err := shell.Exec(javaPath, "--version").ReadOutput()
if err != nil {
log.Fatal(E.Cause(err, "check java version"))
}
if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17")
}
}
func getAndroidBindTarget() string {
if platform != "" {
return platform
} else if debugEnabled {
return "android/arm64"
}
return "android"
}
func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args := []string{
"bind",
"-v",
"-o", config.OutputName,
"-target", bindTarget,
"-androidapi", strconv.Itoa(config.AndroidAPI),
"-androidapi", "21",
"-javapkg=io.nekohasekai",
"-libname=box",
}
if !debugEnabled {
args = append(args, sharedFlags...)
} else {
args = append(args, debugFlags...)
}
args = append(args, "-tags", strings.Join(config.Tags, ","))
args = append(args, "-tags")
if !debugEnabled {
args = append(args, strings.Join(sharedTags, ","))
} else {
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
}
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
@@ -146,87 +91,38 @@ func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
log.Fatal(err)
}
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName))
err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil {
log.Fatal(err)
}
log.Info("copied ", config.OutputName, " to ", copyPath)
log.Info("copied to ", copyPath)
}
}
func buildAndroid() {
build_shared.FindSDK()
checkJavaVersion()
bindTarget := getAndroidBindTarget()
// Build main variant (SDK 24)
mainTags := append([]string{}, sharedTags...)
// mainTags = append(mainTags, memcTags...)
if debugEnabled {
mainTags = append(mainTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 24,
OutputName: "libbox.aar",
Tags: mainTags,
}, bindTarget)
// Build legacy variant (SDK 21, no naive outbound)
legacyTags := filterTags(sharedTags, "with_naive_outbound")
// legacyTags = append(legacyTags, memcTags...)
if debugEnabled {
legacyTags = append(legacyTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 21,
OutputName: "libbox-legacy.aar",
Tags: legacyTags,
}, bindTarget)
}
func buildApple() {
var bindTarget string
if platform != "" {
bindTarget = platform
} else if debugEnabled {
bindTarget = "ios"
} else {
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
}
func buildiOS() {
args := []string{
"bind",
"-v",
"-target", bindTarget,
"-target", "ios,iossimulator,tvos,tvossimulator,macos",
"-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, ","))
//}
if !debugEnabled {
args = append(args, sharedFlags...)
} else {
args = append(args, debugFlags...)
}
tags := append(sharedTags, darwinTags...)
//if withTailscale {
// tags = append(tags, memcTags...)
//}
if debugEnabled {
tags = append(tags, debugTags...)
tags := append(sharedTags, iosTags...)
args = append(args, "-tags")
if !debugEnabled {
args = append(args, strings.Join(tags, ","))
} else {
args = append(args, strings.Join(append(tags, debugTags...), ","))
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
-15
View File
@@ -1,15 +0,0 @@
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, " ")
}
+11 -5
View File
@@ -11,7 +11,9 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/shell"
)
var (
@@ -40,6 +42,14 @@ func FindSDK() {
log.Fatal("android NDK not found")
}
javaVersion, err := shell.Exec("java", "--version").ReadOutput()
if err != nil {
log.Fatal(E.Cause(err, "check java version"))
}
if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17")
}
os.Setenv("ANDROID_HOME", androidSDKPath)
os.Setenv("ANDROID_SDK_HOME", androidSDKPath)
os.Setenv("ANDROID_NDK_HOME", androidNDKPath)
@@ -48,16 +58,12 @@ func FindSDK() {
}
func findNDK() bool {
const fixedVersion = "28.0.13004108"
const fixedVersion = "26.3.11579264"
const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath
return true
}
if ndkHomeEnv := os.Getenv("ANDROID_NDK_HOME"); rw.IsFile(filepath.Join(ndkHomeEnv, versionFile)) {
androidNDKPath = ndkHomeEnv
return true
}
ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk"))
if err != nil {
return false
-10
View File
@@ -3,7 +3,6 @@ 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"
)
@@ -21,11 +20,6 @@ func ReadTag() (string, error) {
return version.String() + "-" + shortCommit, nil
}
func ReadTagVersionRev() (badversion.Version, error) {
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
return badversion.Parse(currentTagRev[1:]), nil
}
func ReadTagVersion() (badversion.Version, error) {
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
@@ -37,7 +31,3 @@ func ReadTagVersion() (badversion.Version, error) {
}
return version, nil
}
func TestFlightVersion(version badversion.Version) string {
return F.ToString(version.Major, ".", version.Minor, ".10")
}
-117
View File
@@ -1,117 +0,0 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/log"
)
func main() {
err := filepath.Walk("docs", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".md") {
return nil
}
return processFile(path)
})
if err != nil {
log.Fatal(err)
}
}
func processFile(path string) error {
content, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
modified := false
result := make([]string, 0, len(lines))
inQuoteBlock := false
materialLines := []int{} // indices of :material- lines in the block
for _, line := range lines {
// Check for quote block start
if strings.HasPrefix(line, "!!! quote \"") && strings.Contains(line, "sing-box") {
inQuoteBlock = true
materialLines = nil
result = append(result, line)
continue
}
// Inside a quote block
if inQuoteBlock {
trimmed := strings.TrimPrefix(line, " ")
isMaterialLine := strings.HasPrefix(trimmed, ":material-")
isEmpty := strings.TrimSpace(line) == ""
isIndented := strings.HasPrefix(line, " ")
if isMaterialLine {
materialLines = append(materialLines, len(result))
result = append(result, line)
continue
}
// Block ends when:
// - Empty line AFTER we've seen material lines, OR
// - Non-indented, non-empty line
blockEnds := (isEmpty && len(materialLines) > 0) || (!isEmpty && !isIndented)
if blockEnds {
// Process collected material lines
if len(materialLines) > 0 {
for j, idx := range materialLines {
isLast := j == len(materialLines)-1
resultLine := strings.TrimRight(result[idx], " ")
if !isLast {
// Add trailing two spaces for non-last lines
resultLine += " "
}
if result[idx] != resultLine {
modified = true
result[idx] = resultLine
}
}
}
inQuoteBlock = false
materialLines = nil
}
}
result = append(result, line)
}
// Handle case where file ends while still in a block
if inQuoteBlock && len(materialLines) > 0 {
for j, idx := range materialLines {
isLast := j == len(materialLines)-1
resultLine := strings.TrimRight(result[idx], " ")
if !isLast {
resultLine += " "
}
if result[idx] != resultLine {
modified = true
result[idx] = resultLine
}
}
}
if modified {
newContent := strings.Join(result, "\n")
if !bytes.Equal(content, []byte(newContent)) {
log.Info("formatted: ", path)
return os.WriteFile(path, []byte(newContent), 0o644)
}
}
return nil
}
-165
View File
@@ -1,165 +0,0 @@
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
}

Some files were not shown because too many files have changed in this diff Show More