Compare commits

...

34 Commits

Author SHA1 Message Date
世界
d3768cca36 Bump version 2026-03-15 17:56:37 +08:00
世界
0889ddd001 Fix connector canceled dial cleanup 2026-03-15 17:56:37 +08:00
深鸣
f46fbf188a documentation: Minor fixes 2026-03-15 17:56:37 +08:00
世界
f2d15139f5 tun: Fix nftables single include_uid not working 2026-03-15 16:58:34 +08:00
世界
041646b728 Fix kTLS crash 2026-03-14 21:38:38 +08:00
世界
b990de2e12 tun: Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" 2026-03-14 21:38:38 +08:00
世界
fe585157d2 Bump version 2026-03-14 21:38:38 +08:00
世界
eed6a36e5d tun:Fix auto_redirect dropping SO_BINDTODEVICE traffic 2026-03-14 21:38:38 +08:00
世界
eb0f38544c tailscale: Fix system interface rules 2026-03-14 21:38:38 +08:00
世界
54468a1a2a platform: Add f-droid update helpers 2026-03-11 20:41:29 +08:00
世界
8289bbd846 Add Alpine APK packaging to CI build
Add fpm-based Alpine APK packaging alongside existing DEB/RPM/Pacman
packages. Alpine APKs use `linux` in the filename to distinguish from
OpenWrt APKs which use the `openwrt` prefix.
2026-03-11 20:41:29 +08:00
世界
49c450d942 ccm/ocm: Fix missing metering for 1M context and /fast mode
CCM: Fix 1M context detection - use prefix match for versioned
beta strings (e.g. "context-1m-2025-08-07") and include cache
tokens in the 200K threshold check per Anthropic billing docs.

OCM: Add GPT-5.4 family pricing (standard/priority/flex) with
extended context (>272K) premium pricing support. Add context
window tracking to usage combinations, mirroring CCM's pattern.
Update normalizeGPT5Model defaults to latest known models.
2026-03-11 20:41:29 +08:00
世界
a7ee943216 Fix tailscale connections 2026-03-11 00:27:15 +08:00
世界
8bb4c4dd32 documentation: Update ocm/ccm examples 2026-03-10 22:04:12 +08:00
世界
67621ee6ba Fix OCM websocket proxy lifecycle and headers 2026-03-10 22:04:11 +08:00
世界
a09ffe6a0f ccm/ocm: Add by_user_and_week cost summary 2026-03-10 22:04:11 +08:00
世界
e0be8743f6 ocm: Add Responses WebSocket API proxy and fix client config docs
Support the OpenAI Responses WebSocket API (`wss://.../v1/responses`)
for bidirectional frame proxying with usage tracking.
Fix Codex CLI client config examples to use profiles and correct flags.

Update openai-go v3.24.0 → v3.26.0.
2026-03-10 22:04:11 +08:00
世界
0b04528803 tailscaile: Fix using TUN auto redirect with tailscale system interface 2026-03-10 22:04:11 +08:00
世界
65875e6dac tailscale: Use system dialer for system interface
* Revert "Fix netstack TCP connections with system interface
2026-03-10 19:50:16 +08:00
世界
4d6fb1d38d Fix legacy DNS client_subnet options not working 2026-03-09 20:18:47 +08:00
世界
305b930d90 release: Fix default config 2026-03-09 20:18:43 +08:00
世界
bc3884ca91 release: Add openwrt apk build 2026-03-09 20:18:40 +08:00
世界
df0bf927e4 Fix missing with_gvisor build tag for tailscale 2026-03-09 20:18:28 +08:00
世界
efe20ea51c release: Backport Go 1.25 to macOS 10.13 2026-03-09 20:13:36 +08:00
世界
e21a72fcd1 Fix websocket connection and goroutine leaks in Clash API
Co-authored-by: traitman <112139837+traitman@users.noreply.github.com>
2026-03-09 20:06:34 +08:00
世界
e1477bd065 documentation: Update cronet-go descriptions 2026-03-09 20:06:34 +08:00
世界
aa495fce38 Fix local DNS transport CNAME chain broken with systemd-resolved
Replace D-Bus ResolveRecord API with direct raw DNS queries to upstream
servers obtained from systemd-resolved's per-interface link properties.
2026-03-09 20:06:34 +08:00
世界
9cd60c28c0 tailscale: Fix inbound UDP packet connection 2026-03-09 20:06:34 +08:00
Heng lu
2ba896c5ac Fix netns fd leak in ListenNetworkNamespace 2026-03-09 20:06:34 +08:00
Oleg Artyomov
1d388547ee service/ccm: strip Accept-Encoding before forwarding to avoid untracked usage
When clients (e.g. Node.js Anthropic SDK) explicitly set Accept-Encoding: gzip,
Go's http.Transport does not transparently decompress the response body, because
it only does so when it added the header itself. This causes CCM's json.Unmarshal
to receive raw gzip bytes, silently failing to parse usage data and leaving the
usage counter unchanged.

Fix: remove Accept-Encoding from the outgoing proxy request. Transport adds it
automatically and transparently decompresses response.Body before CCM reads it.

Wire compression (CCM→Anthropic) is preserved — Transport still negotiates gzip.
Only CCM→localhost path is affected; compression on loopback has no practical
benefit.
2026-03-09 20:06:34 +08:00
世界
e343cec4d5 Fix legacy DNS defaults on final transport 2026-03-09 20:06:34 +08:00
世界
d58efc5d01 cronet-go: Fix library search path 2026-03-09 20:06:34 +08:00
世界
4b26ab16fb Bump version 2026-03-07 16:13:23 +08:00
世界
0e27312eda Update Go to 1.25.8 2026-03-07 16:13:23 +08:00
55 changed files with 2461 additions and 416 deletions

View File

@@ -1 +1 @@
cba7b9ac0399055aa49fbdc57c03c374f58e1597
2fef65f9dba90ddb89a87d00a6eb6165487c10c1

81
.github/build_alpine_apk.sh vendored Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -e -o pipefail
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)
trap 'rm -rf "$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 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"

80
.github/build_openwrt_apk.sh vendored Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -e -o pipefail
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)
trap 'rm -rf "$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 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" \
--info "origin:sing-box" \
--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 "provider-priority:100" \
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

45
.github/setup_go_for_macos1013.sh vendored Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.25.8"
PATCH_COMMITS=(
"afe69d3cec1c6dcf0f1797b20546795730850070"
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
)
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.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
# 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"
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"

View File

@@ -1,16 +1,35 @@
#!/usr/bin/env bash
VERSION="1.25.7"
set -euo pipefail
mkdir -p $HOME/go
cd $HOME/go
VERSION="1.25.8"
PATCH_COMMITS=(
"466f6c7a29bc098b0d4c987b803c779222894a11"
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
"a90777dcf692dd2168577853ba743b4338721b06"
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
)
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
# this patch file only works on golang1.25.x
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert:
@@ -18,10 +37,10 @@ cd go_win7
# 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\""
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done

View File

@@ -47,7 +47,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -72,27 +72,27 @@ jobs:
include:
- { os: linux, arch: amd64, variant: purego, naive: true }
- { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true }
- { os: linux, arch: arm64, variant: glibc, naive: true }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: "386", go386: sse2 }
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" }
- { os: linux, arch: arm, goarm: "7" }
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "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" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, openwrt: "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" }
- { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
- { os: linux, arch: riscv64, naive: true, variant: glibc }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, naive: true, variant: glibc }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
@@ -121,15 +121,10 @@ jobs:
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
if: ${{ ! matrix.legacy_win7 }}
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.10
go-version: ~1.25.8
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
@@ -137,9 +132,11 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1255
key: go_win7_1258
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |-
.github/setup_go_for_windows7.sh
- name: Setup Go for Windows 7
@@ -399,6 +396,30 @@ jobs:
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done
rm "dist/openwrt.deb"
- name: Install apk-tools
if: matrix.openwrt != '' || matrix.alpine != ''
run: |-
docker run --rm -v /usr/local/bin:/mnt alpine:edge sh -c "apk add --no-cache apk-tools-static && cp /sbin/apk.static /mnt/apk && chmod +x /mnt/apk"
- name: Package OpenWrt APK
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
for architecture in ${{ matrix.openwrt }}; do
.github/build_openwrt_apk.sh \
"$architecture" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.apk"
done
- name: Package Alpine APK
if: matrix.alpine != ''
run: |-
set -xeuo pipefail
.github/build_alpine_apk.sh \
"${{ matrix.alpine }}" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.alpine }}.apk"
- name: Archive
run: |
set -xeuo pipefail
@@ -434,22 +455,36 @@ jobs:
include:
- { arch: amd64 }
- { arch: arm64 }
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! matrix.legacy_go124 }}
if: ${{ ! matrix.legacy_osx }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
- name: Cache Go for macOS 10.13
if: matrix.legacy_osx
id: cache-go-for-macos1013
uses: actions/cache@v4
with:
go-version: ~1.24.6
path: |
~/go/go_osx
key: go_osx_1258
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |-
.github/setup_go_for_macos1013.sh
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx
run: |-
echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -457,7 +492,7 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
@@ -477,6 +512,7 @@ jobs:
CGO_ENABLED: "1"
GOOS: darwin
GOARCH: ${{ matrix.arch }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name
run: |-
@@ -605,7 +641,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -695,7 +731,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -794,7 +830,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Set tag
if: matrix.if
run: |-

View File

@@ -56,7 +56,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Clone cronet-go
if: matrix.naive
run: |

View File

@@ -35,7 +35,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -78,7 +78,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.7
go-version: ~1.25.8
- name: Clone cronet-go
if: matrix.naive
run: |

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"net"
"unsafe"
)
func (c *Conn) Read(b []byte) (int, error) {
@@ -229,7 +230,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
data, typ, err = c.rawConn.In.Decrypt(record)
if err != nil {
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
return
}
return

View File

@@ -151,6 +151,7 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (
if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get current netns")
}
defer currentNs.Close()
defer netns.Set(currentNs)
var targetNs netns.NsHandle
if strings.HasPrefix(nameOrPath, "/") {

View File

@@ -324,16 +324,20 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
} else {
strategy = options.Strategy
}
lookupOptions := options
if options.LookupStrategy != C.DomainStrategyAsIS {
lookupOptions.Strategy = strategy
}
if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
} else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
}
var response4 []netip.Addr
var response6 []netip.Addr
var group task.Group
group.Append("exchange4", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
if err != nil {
return err
}
@@ -341,7 +345,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
return nil
})
group.Append("exchange6", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
if err != nil {
return err
}

View File

@@ -195,7 +195,16 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
}
}
}
return r.transport.Default(), nil, -1
transport := r.transport.Default()
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
return transport, nil, -1
}
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
@@ -345,7 +354,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
transport := options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()

View File

@@ -55,6 +55,12 @@ type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
type connectorDialResult[T any] struct {
connection T
cancel context.CancelFunc
err error
}
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
@@ -100,41 +106,37 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
return zero, err
}
c.connecting = make(chan struct{})
connecting := make(chan struct{})
c.connecting = connecting
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
dialResult := make(chan connectorDialResult[T], 1)
c.access.Unlock()
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
connection, cancel, err := c.dialWithCancellation(dialContext)
go func() {
connection, cancel, err := c.dialWithCancellation(dialContext)
dialResult <- connectorDialResult[T]{
connection: connection,
cancel: cancel,
err: err,
}
}()
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
select {
case result := <-dialResult:
return c.completeDial(ctx, connecting, result)
case <-ctx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ctx.Err()
case <-c.closeCtx.Done():
go func() {
result := <-dialResult
_, _ = c.completeDial(ctx, connecting, result)
}()
return zero, ErrTransportClosed
}
if err = ctx.Err(); err != nil {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, err
}
c.connection = connection
c.hasConnection = true
c.connectionCancel = cancel
result := c.connection
c.access.Unlock()
return result, nil
}
}
@@ -143,6 +145,38 @@ func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T
return loaded && dialConnector == connector
}
func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) {
var zero T
c.access.Lock()
defer c.access.Unlock()
defer func() {
if c.connecting == connecting {
c.connecting = nil
}
close(connecting)
}()
if result.err != nil {
return zero, result.err
}
if c.closed || c.closeCtx.Err() != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, ErrTransportClosed
}
if err := ctx.Err(); err != nil {
result.cancel()
c.callbacks.Close(result.connection)
return zero, err
}
c.connection = result.connection
c.hasConnection = true
c.connectionCancel = result.cancel
return c.connection, nil
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {

View File

@@ -188,13 +188,157 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
err := <-result
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 1, closeCount.Load())
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err = connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
dialStarted := make(chan struct{}, 1)
releaseDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
select {
case dialStarted <- struct{}{}:
default:
}
<-releaseDial
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
result <- err
}()
<-dialStarted
cancel()
select {
case err := <-result:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("Get did not return after request cancel")
}
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 0, closeCount.Load())
close(releaseDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
_, err := connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
)
firstDialStarted := make(chan struct{}, 1)
secondDialStarted := make(chan struct{}, 1)
releaseFirstDial := make(chan struct{})
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
attempt := dialCount.Add(1)
switch attempt {
case 1:
select {
case firstDialStarted <- struct{}{}:
default:
}
<-releaseFirstDial
case 2:
select {
case secondDialStarted <- struct{}{}:
default:
}
}
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
firstResult := make(chan error, 1)
go func() {
_, err := connector.Get(requestContext)
firstResult <- err
}()
<-firstDialStarted
cancel()
secondResult := make(chan error, 1)
go func() {
_, err := connector.Get(context.Background())
secondResult <- err
}()
select {
case <-secondDialStarted:
t.Fatal("second dial started before first dial completed")
case <-time.After(100 * time.Millisecond):
}
select {
case err := <-firstResult:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("first Get did not return after request cancel")
}
close(releaseFirstDial)
require.Eventually(t, func() bool {
return closeCount.Load() == 1
}, time.Second, 10*time.Millisecond)
select {
case <-secondDialStarted:
case <-time.After(time.Second):
t.Fatal("second dial did not start after first dial completed")
}
err := <-secondResult
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
t.Parallel()

View File

@@ -81,10 +81,7 @@ func (t *Transport) Reset() {
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil {
resolverObject := t.resolved.Object()
if resolverObject != nil {
return t.resolved.Exchange(resolverObject, ctx, message)
}
return t.resolved.Exchange(ctx, message)
}
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

View File

@@ -9,6 +9,5 @@ import (
type ResolvedResolver interface {
Start() error
Close() error
Object() any
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
}

View File

@@ -4,19 +4,26 @@ import (
"bufio"
"context"
"errors"
"net/netip"
"os"
"strings"
"sync"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
dnsTransport "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
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"
@@ -49,13 +56,23 @@ type DBusResolvedResolver struct {
interfaceMonitor tun.DefaultInterfaceMonitor
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
systemBus *dbus.Conn
resoledObject atomic.Pointer[ResolvedObject]
savedServerSet atomic.Pointer[resolvedServerSet]
closeOnce sync.Once
}
type ResolvedObject struct {
dbus.BusObject
InterfaceIndex int32
type resolvedServerSet struct {
servers []resolvedServer
}
type resolvedServer struct {
primaryTransport adapter.DNSTransport
fallbackTransport adapter.DNSTransport
}
type resolvedServerSpecification struct {
address netip.Addr
port uint16
serverName string
}
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
@@ -82,17 +99,31 @@ func (t *DBusResolvedResolver) Start() error {
"org.freedesktop.DBus",
"NameOwnerChanged",
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1"),
).Err
if err != nil {
return E.Cause(err, "configure resolved restart listener")
}
err = t.systemBus.BusObject().AddMatchSignal(
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
dbus.WithMatchSender("org.freedesktop.resolve1"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
).Err
if err != nil {
return E.Cause(err, "configure resolved properties listener")
}
go t.loopUpdateStatus()
return nil
}
func (t *DBusResolvedResolver) Close() error {
var closeErr error
t.closeOnce.Do(func() {
serverSet := t.savedServerSet.Swap(nil)
if serverSet != nil {
closeErr = serverSet.Close()
}
if t.interfaceCallback != nil {
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
}
@@ -100,99 +131,97 @@ func (t *DBusResolvedResolver) Close() error {
_ = t.systemBus.Close()
}
})
return nil
return closeErr
}
func (t *DBusResolvedResolver) Object() any {
return common.PtrOrNil(t.resoledObject.Load())
}
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
resolvedObject := object.(*ResolvedObject)
call := resolvedObject.CallWithContext(
ctx,
"org.freedesktop.resolve1.Manager.ResolveRecord",
0,
resolvedObject.InterfaceIndex,
question.Name,
question.Qclass,
question.Qtype,
uint64(0),
)
if call.Err != nil {
var dbusError dbus.Error
if errors.As(call.Err, &dbusError) && dbusError.Name == "org.freedesktop.resolve1.NoNameServers" {
t.updateStatus()
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
serverSet := t.savedServerSet.Load()
if serverSet == nil {
var err error
serverSet, err = t.checkResolved(context.Background())
if err != nil {
return nil, err
}
previousServerSet := t.savedServerSet.Swap(serverSet)
if previousServerSet != nil {
_ = previousServerSet.Close()
}
return nil, E.Cause(call.Err, " resolve record via resolved")
}
var (
records []resolved.ResourceRecord
outflags uint64
)
err := call.Store(&records, &outflags)
if err != nil {
response, err := t.exchangeServerSet(ctx, message, serverSet)
if err == nil {
return response, nil
}
t.updateStatus()
refreshedServerSet := t.savedServerSet.Load()
if refreshedServerSet == nil || refreshedServerSet == serverSet {
return nil, err
}
response := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: mDNS.RcodeSuccess,
},
Question: []mDNS.Question{question},
}
for _, record := range records {
var rr mDNS.RR
rr, _, err = mDNS.UnpackRR(record.Data, 0)
if err != nil {
return nil, E.Cause(err, "unpack resource record")
}
response.Answer = append(response.Answer, rr)
}
return response, nil
return t.exchangeServerSet(ctx, message, refreshedServerSet)
}
func (t *DBusResolvedResolver) loopUpdateStatus() {
signalChan := make(chan *dbus.Signal, 1)
t.systemBus.Signal(signalChan)
for signal := range signalChan {
var restarted bool
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
switch signal.Name {
case "org.freedesktop.DBus.NameOwnerChanged":
if len(signal.Body) != 3 {
continue
}
newOwner, loaded := signal.Body[2].(string)
if !loaded || newOwner == "" {
continue
}
t.updateStatus()
case "org.freedesktop.DBus.Properties.PropertiesChanged":
if !shouldUpdateResolvedServerSet(signal) {
continue
} else {
restarted = true
}
}
if restarted {
t.updateStatus()
}
}
}
func (t *DBusResolvedResolver) updateStatus() {
dbusObject, err := t.checkResolved(context.Background())
oldValue := t.resoledObject.Swap(dbusObject)
serverSet, err := t.checkResolved(context.Background())
oldServerSet := t.savedServerSet.Swap(serverSet)
if oldServerSet != nil {
_ = oldServerSet.Close()
}
if err != nil {
var dbusErr dbus.Error
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" {
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" {
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
}
if oldValue != nil {
if oldServerSet != nil {
t.logger.Debug("systemd-resolved service is gone")
}
return
} else if oldValue == nil {
} else if oldServerSet == nil {
t.logger.Debug("using systemd-resolved service as resolver")
}
}
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) {
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) {
if serverSet == nil || len(serverSet.servers) == 0 {
return nil, E.New("link has no DNS servers configured")
}
var lastError error
for _, server := range serverSet.servers {
response, err := server.primaryTransport.Exchange(ctx, message)
if err != nil && server.fallbackTransport != nil {
response, err = server.fallbackTransport.Exchange(ctx, message)
}
if err != nil {
lastError = err
continue
}
return response, nil
}
return nil, lastError
}
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
if err != nil {
@@ -220,16 +249,19 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
if linkObject == nil {
return nil, E.New("missing link object for default interface")
}
dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
if err != nil {
return nil, err
}
var linkDNS []resolved.LinkDNS
err = dnsProp.Store(&linkDNS)
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
if err != nil {
return nil, err
}
if len(linkDNS) == 0 {
linkDNS, err := loadResolvedLinkDNS(linkObject)
if err != nil {
return nil, err
}
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
if inbound.Type() == C.TypeTun {
return nil, E.New("No appropriate name servers or networks for name found")
@@ -237,12 +269,233 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
}
return nil, E.New("link has no DNS servers configured")
}
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: defaultInterface.Name,
UDPFragmentDefault: true,
})
if err != nil {
return nil, err
}
var serverSpecifications []resolvedServerSpecification
if len(linkDNSEx) > 0 {
for _, entry := range linkDNSEx {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
} else {
for _, entry := range linkDNS {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
}
if len(serverSpecifications) == 0 {
return nil, E.New("no valid DNS servers on link")
}
serverSet := &resolvedServerSet{
servers: make([]resolvedServer, 0, len(serverSpecifications)),
}
for _, serverSpecification := range serverSpecifications {
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
if createErr != nil {
_ = serverSet.Close()
return nil, createErr
}
serverSet.servers = append(serverSet.servers, server)
}
return serverSet, nil
}
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
if dnsOverTLSMode == "yes" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
}, nil
}
if dnsOverTLSMode == "opportunistic" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
if err != nil {
_ = primaryTransport.Close()
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
fallbackTransport: fallbackTransport,
}, nil
}
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
if err != nil {
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
}, nil
}
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
if useTLS {
tlsAddress := serverSpecification.address
if tlsAddress.Zone() != "" {
tlsAddress = tlsAddress.WithZone("")
}
serverName := serverSpecification.serverName
if serverName == "" {
serverName = tlsAddress.String()
}
tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{
Enabled: true,
ServerName: serverName,
})
if err != nil {
return nil, err
}
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
err = serverTransport.Start(adapter.StartStateStart)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
err := serverTransport.Start(adapter.StartStateStart)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
func (s *resolvedServerSet) Close() error {
var errors []error
for _, server := range s.servers {
errors = append(errors, server.primaryTransport.Close())
if server.fallbackTransport != nil {
errors = append(errors, server.fallbackTransport.Close())
}
}
return E.Errors(errors...)
}
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
address, loaded := netip.AddrFromSlice(rawAddress)
if !loaded {
return resolvedServerSpecification{}, false
}
if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" {
address = address.WithZone(interfaceName)
}
return resolvedServerSpecification{
address: address,
port: port,
serverName: serverName,
}, true
}
func resolvedServerPort(port uint16, useTLS bool) uint16 {
if port > 0 {
return port
}
if useTLS {
return 853
}
return 53
}
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
}
var linkDNS []resolved.LinkDNS
err = dnsProperty.Store(&linkDNS)
if err != nil {
return nil, err
}
return linkDNS, nil
}
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
}
var linkDNSEx []resolved.LinkDNSEx
err = dnsProperty.Store(&linkDNSEx)
if err != nil {
return nil, err
}
return linkDNSEx, nil
}
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return "", nil
}
return "", err
}
var dnsOverTLSMode string
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
if err != nil {
return "", err
}
return dnsOverTLSMode, nil
}
func isResolvedUnknownPropertyError(err error) bool {
var dbusError dbus.Error
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
}
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
if len(signal.Body) != 3 {
return true
}
changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant)
if !loaded {
return true
}
for propertyName := range changedProperties {
switch propertyName {
case "DNS", "DNSEx", "DNSOverTLS":
return true
}
}
invalidatedProperties, loaded := signal.Body[2].([]string)
if !loaded {
return true
}
for _, propertyName := range invalidatedProperties {
switch propertyName {
case "DNS", "DNSEx", "DNSOverTLS":
return true
}
}
return false
}
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {
t.updateStatus()
}

View File

@@ -2,6 +2,34 @@
icon: material/alert-decagram
---
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.13.2
* Fixes and improvements
#### 1.13.1
* Fixes and improvements

View File

@@ -38,7 +38,7 @@ icon: material/alert-decagram
!!! warning "与官方 Hysteria2 的区别"
官方程序支持一种名为 **userpass** 的验证方式,
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
### 监听字段

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.13.3"
:material-alert: [strict_route](#strict_route)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
@@ -348,6 +352,9 @@ Enforce strict routing rules when `auto_route` is enabled:
* Let unsupported network unreachable
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN.
* When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic:
* Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box.
* Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box.
*In Windows*:

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "sing-box 1.13.3 中的更改"
:material-alert: [strict_route](#strict_route)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
@@ -347,6 +351,9 @@ tun 接口的 IPv6 前缀。
* 使不支持的网络不可达。
* 出于历史遗留原因,当未启用 `strict_route``auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量:
* 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。
* 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。
*在 Windows 中*

View File

@@ -38,7 +38,7 @@
!!! warning "与官方 Hysteria2 的区别"
官方程序支持一种名为 **userpass** 的验证方式,
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
### 字段

View File

@@ -34,10 +34,12 @@ icon: material/new-box
| Build Variant | Platforms | Description |
|---------------|-----------|-------------|
| (default) | Linux amd64/arm64 | purego build with `libcronet.so` included |
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO build dynamically linked with glibc, requires glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64 | CGO build statically linked with musl, no system requirements |
| (default) | Windows amd64/arm64 | purego build with `libcronet.dll` included |
| (no suffix) | Linux amd64/arm64 | purego build, `libcronet.so` included |
| `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO build, dynamically linked with glibc, requires glibc >= 2.31 (loong64: >= 2.36) |
| `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO build, statically linked with musl |
| (no suffix) | Windows amd64/arm64 | purego build, `libcronet.dll` included |
For Linux, choose the glibc or musl variant based on your distribution's libc type.
**Runtime Requirements:**

View File

@@ -32,12 +32,14 @@ icon: material/new-box
**官方发布版本区别:**
| 构建变体 | 平台 | 说明 |
|-----------|------------------------|------------------------------------------|
| (默认) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO 构建,动态链接 glibc要求 glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64 | CGO 构建,静态链接 musl,无系统要求 |
| (默认) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
| 构建变体 | 平台 | 说明 |
|---|---|---|
| (无后缀) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
| `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO 构建,动态链接 glibc要求 glibc >= 2.31loong64: >= 2.36 |
| `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO 构建,静态链接 musl |
| (无后缀) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
对于 Linux请根据发行版的 libc 类型选择 glibc 或 musl 变体。
**运行时要求:**

View File

@@ -66,7 +66,19 @@ List of authorized users for token authentication.
If empty, no authentication is required.
Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
Object format:
```json
{
"name": "",
"token": ""
}
```
Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
#### headers
@@ -84,23 +96,36 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### Example
#### Server
```json
{
"services": [
{
"type": "ccm",
"listen": "127.0.0.1",
"listen_port": 8080
"listen": "0.0.0.0",
"listen_port": 8080,
"usages_path": "./claude-usages.json",
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob"
}
]
}
]
}
```
Connect to the CCM service:
#### Client
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
claude
```

View File

@@ -66,7 +66,19 @@ Claude Code OAuth 凭据文件的路径。
如果为空,则不需要身份验证。
Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
对象格式:
```json
{
"name": "",
"token": ""
}
```
对象字段:
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
#### headers
@@ -84,23 +96,36 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
### 示例
#### 服务端
```json
{
"services": [
{
"type": "ccm",
"listen": "127.0.0.1",
"listen_port": 8080
"listen": "0.0.0.0",
"listen_port": 8080,
"usages_path": "./claude-usages.json",
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob"
}
]
}
]
}
```
连接到 CCM 服务:
#### 客户端
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
claude
```

View File

@@ -37,7 +37,9 @@ See [Listen Fields](/configuration/shared/listen/) for details.
Path to the OpenAI OAuth credentials file.
If not specified, defaults to `~/.codex/auth.json`.
If not specified, defaults to:
- `$CODEX_HOME/auth.json` if `CODEX_HOME` environment variable is set
- `~/.codex/auth.json` otherwise
Refreshed tokens are automatically written back to the same location.
@@ -111,17 +113,23 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
Add to `~/.codex/config.toml`:
```toml
# profile = "ocm" # set as default profile
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = true
[profiles.ocm]
model_provider = "ocm"
# model = "gpt-5.4" # if the latest model is not yet publicly released
# model_reasoning_effort = "xhigh"
```
Then run:
```bash
codex --model-provider ocm
codex --profile ocm
```
### Example with Authentication
@@ -139,11 +147,11 @@ codex --model-provider ocm
"users": [
{
"name": "alice",
"token": "sk-alice-secret-token"
"token": "sk-ocm-hello-world"
},
{
"name": "bob",
"token": "sk-bob-secret-token"
"token": "sk-ocm-hello-bob"
}
]
}
@@ -156,16 +164,22 @@ codex --model-provider ocm
Add to `~/.codex/config.toml`:
```toml
# profile = "ocm" # set as default profile
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "sk-alice-secret-token"
supports_websockets = true
experimental_bearer_token = "sk-ocm-hello-world"
[profiles.ocm]
model_provider = "ocm"
# model = "gpt-5.4" # if the latest model is not yet publicly released
# model_reasoning_effort = "xhigh"
```
Then run:
```bash
codex --model-provider ocm
codex --profile ocm
```

View File

@@ -37,7 +37,9 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
OpenAI OAuth 凭据文件的路径。
如果未指定,默认值为 `~/.codex/auth.json`
如果未指定,默认值为
- 如果设置了 `CODEX_HOME` 环境变量,则使用 `$CODEX_HOME/auth.json`
- 否则使用 `~/.codex/auth.json`
刷新的令牌会自动写回相同位置。
@@ -111,17 +113,24 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
`~/.codex/config.toml` 中添加:
```toml
# profile = "ocm" # 设为默认配置
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = true
[profiles.ocm]
model_provider = "ocm"
# model = "gpt-5.4" # 如果最新模型尚未公开发布
# model_reasoning_effort = "xhigh"
```
然后运行:
```bash
codex --model-provider ocm
codex --profile ocm
```
### 带身份验证的示例
@@ -139,11 +148,11 @@ codex --model-provider ocm
"users": [
{
"name": "alice",
"token": "sk-alice-secret-token"
"token": "sk-ocm-hello-world"
},
{
"name": "bob",
"token": "sk-bob-secret-token"
"token": "sk-ocm-hello-bob"
}
]
}
@@ -156,16 +165,22 @@ codex --model-provider ocm
`~/.codex/config.toml` 中添加:
```toml
# profile = "ocm" # 设为默认配置
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "sk-alice-secret-token"
supports_websockets = true
experimental_bearer_token = "sk-ocm-hello-world"
[profiles.ocm]
model_provider = "ocm"
# model = "gpt-5.4" # 如果最新模型尚未公开发布
# model_reasoning_effort = "xhigh"
```
然后运行:
```bash
codex --model-provider ocm
codex --profile ocm
```

View File

@@ -92,14 +92,14 @@ NaiveProxy outbound requires special build configurations depending on your targ
### Supported Platforms
| Platform | Architectures | Mode | Requirements |
|-----------------|------------------------|--------|---------------------------------------------------|
| Linux | amd64, arm64 | purego | None (library included in official releases) |
| Linux | 386, amd64, arm, arm64 | CGO | Chromium toolchain, glibc >= 2.31 at runtime |
| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium toolchain |
| Windows | amd64, arm64 | purego | None (library included in official releases) |
| Apple platforms | * | CGO | Xcode |
| Android | * | CGO | Android NDK |
| Platform | Architectures | Mode | Requirements |
|-----------------|--------------------------------------------------------|--------|-----------------------------------------------------------------|
| Linux | amd64, arm64 | purego | None (library included in official releases) |
| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium toolchain, glibc >= 2.31 (loong64: >= 2.36) at runtime |
| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium toolchain |
| Windows | amd64, arm64 | purego | None (library included in official releases) |
| Apple platforms | * | CGO | Xcode |
| Android | * | CGO | Android NDK |
### Windows

View File

@@ -96,14 +96,14 @@ NaiveProxy 出站需要根据目标平台进行特殊的构建配置。
### 支持的平台
| 平台 | 架构 | 模式 | 要求 |
|---------------|------------------------|--------|--------------------------------|
| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
| Linux | 386, amd64, arm, arm64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31 |
| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium 工具链 |
| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
| Apple 平台 | * | CGO | Xcode |
| Android | * | CGO | Android NDK |
| 平台 | 架构 | 模式 | 要求 |
|--------------|----------------------------------------------------------|--------|-----------------------------------------------------|
| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31loong64: >= 2.36 |
| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium 工具链 |
| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
| Apple 平台 | * | CGO | Xcode |
| Android | * | CGO | Android NDK |
### Windows

View File

@@ -47,6 +47,17 @@ elif command -v rpm >/dev/null 2>&1; then
arch=$(uname -m)
package_suffix=".rpm"
package_install="rpm -i"
elif command -v apk >/dev/null 2>&1 && [ -f /etc/os-release ] && grep -q OPENWRT_ARCH /etc/os-release; then
os="openwrt"
. /etc/os-release
arch="$OPENWRT_ARCH"
package_suffix=".apk"
package_install="apk add --allow-untrusted"
elif command -v apk >/dev/null 2>&1; then
os="linux"
arch=$(apk --print-arch)
package_suffix=".apk"
package_install="apk add --allow-untrusted"
elif command -v opkg >/dev/null 2>&1; then
os="openwrt"
. /etc/os-release

View File

@@ -2,6 +2,7 @@ package clashapi
import (
"bytes"
"context"
"net"
"net/http"
"runtime/debug"
@@ -27,7 +28,7 @@ func (s *Server) setupMetaAPI(r chi.Router) {
})
r.Mount("/", middleware.Profiler())
}
r.Get("/memory", memory(s.trafficManager))
r.Get("/memory", memory(s.ctx, s.trafficManager))
r.Mount("/group", groupRouter(s))
r.Mount("/upgrade", upgradeRouter(s))
}
@@ -37,7 +38,7 @@ type Memory struct {
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
}
func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var conn net.Conn
if r.Header.Get("Upgrade") == "websocket" {
@@ -46,6 +47,7 @@ func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r
if err != nil {
return
}
defer conn.Close()
}
if conn == nil {
@@ -58,7 +60,12 @@ func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r
buf := &bytes.Buffer{}
var err error
first := true
for range tick.C {
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
buf.Reset()
inuse := trafficManager.Snapshot().Memory

View File

@@ -38,6 +38,7 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
if err != nil {
return
}
defer conn.Close()
intervalStr := r.URL.Query().Get("interval")
interval := 1000

View File

@@ -115,7 +115,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
chiRouter.Group(func(r chi.Router) {
r.Use(authentication(options.Secret))
r.Get("/", hello(options.ExternalUI != ""))
r.Get("/logs", getLogs(logFactory))
r.Get("/logs", getLogs(s.ctx, logFactory))
r.Get("/traffic", traffic(s.ctx, trafficManager))
r.Get("/version", version)
r.Mount("/configs", configRouter(s, logFactory))
@@ -360,7 +360,7 @@ type Log struct {
Payload string `json:"payload"`
}
func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
levelText := r.URL.Query().Get("level")
if levelText == "" {
@@ -399,6 +399,8 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht
var logEntry log.Entry
for {
select {
case <-ctx.Done():
return
case <-done:
return
case logEntry = <-subscription:

View File

@@ -0,0 +1,493 @@
package libbox
import (
"archive/zip"
"bytes"
"crypto/tls"
"encoding/json"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
const fdroidUserAgent = "F-Droid 1.21.1"
type FDroidUpdateInfo struct {
VersionCode int32
VersionName string
DownloadURL string
FileSize int64
FileSHA256 string
}
type FDroidPingResult struct {
URL string
LatencyMs int32
Error string
}
type FDroidPingResultIterator interface {
Len() int32
HasNext() bool
Next() *FDroidPingResult
}
type fdroidAPIResponse struct {
PackageName string `json:"packageName"`
SuggestedVersionCode int32 `json:"suggestedVersionCode"`
Packages []fdroidAPIPackage `json:"packages"`
}
type fdroidAPIPackage struct {
VersionName string `json:"versionName"`
VersionCode int32 `json:"versionCode"`
}
type fdroidEntry struct {
Timestamp int64 `json:"timestamp"`
Version int `json:"version"`
Index fdroidEntryFile `json:"index"`
Diffs map[string]fdroidEntryFile `json:"diffs"`
}
type fdroidEntryFile struct {
Name string `json:"name"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
NumPackages int `json:"numPackages"`
}
type fdroidIndexV2 struct {
Packages map[string]fdroidV2Package `json:"packages"`
}
type fdroidV2Package struct {
Versions map[string]fdroidV2Version `json:"versions"`
}
type fdroidV2Version struct {
Manifest fdroidV2Manifest `json:"manifest"`
File fdroidV2File `json:"file"`
}
type fdroidV2Manifest struct {
VersionCode int32 `json:"versionCode"`
VersionName string `json:"versionName"`
}
type fdroidV2File struct {
Name string `json:"name"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
}
type fdroidIndexV1 struct {
Packages map[string][]fdroidV1Package `json:"packages"`
}
type fdroidV1Package struct {
VersionCode int32 `json:"versionCode"`
VersionName string `json:"versionName"`
ApkName string `json:"apkName"`
Size int64 `json:"size"`
Hash string `json:"hash"`
HashType string `json:"hashType"`
}
type fdroidCache struct {
MirrorURL string `json:"mirrorURL"`
Timestamp int64 `json:"timestamp"`
ETag string `json:"etag"`
IsV1 bool `json:"isV1,omitempty"`
}
func CheckFDroidUpdate(mirrorURL, packageName string, currentVersionCode int32, cachePath string) (*FDroidUpdateInfo, error) {
mirrorURL = strings.TrimRight(mirrorURL, "/")
if strings.Contains(mirrorURL, "f-droid.org") {
return checkFDroidAPI(mirrorURL, packageName, currentVersionCode)
}
client := newFDroidHTTPClient()
defer client.CloseIdleConnections()
cache := loadFDroidCache(cachePath, mirrorURL)
if cache != nil && cache.IsV1 {
return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, cache)
}
return checkFDroidV2(client, mirrorURL, packageName, currentVersionCode, cachePath, cache)
}
func PingFDroidMirrors(mirrorURLs string) (FDroidPingResultIterator, error) {
urls := strings.Split(mirrorURLs, ",")
results := make([]*FDroidPingResult, len(urls))
var waitGroup sync.WaitGroup
for i, rawURL := range urls {
waitGroup.Add(1)
go func(index int, target string) {
defer waitGroup.Done()
target = strings.TrimSpace(target)
result := &FDroidPingResult{URL: target}
latency, err := pingTLS(target)
if err != nil {
result.LatencyMs = -1
result.Error = err.Error()
} else {
result.LatencyMs = int32(latency.Milliseconds())
}
results[index] = result
}(i, rawURL)
}
waitGroup.Wait()
sort.Slice(results, func(i, j int) bool {
if results[i].LatencyMs < 0 {
return false
}
if results[j].LatencyMs < 0 {
return true
}
return results[i].LatencyMs < results[j].LatencyMs
})
return newIterator(results), nil
}
func PingFDroidMirror(mirrorURL string) *FDroidPingResult {
mirrorURL = strings.TrimSpace(mirrorURL)
result := &FDroidPingResult{URL: mirrorURL}
latency, err := pingTLS(mirrorURL)
if err != nil {
result.LatencyMs = -1
result.Error = err.Error()
} else {
result.LatencyMs = int32(latency.Milliseconds())
}
return result
}
func newFDroidHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
}
}
func newFDroidRequest(requestURL string) (*http.Request, error) {
request, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", fdroidUserAgent)
return request, nil
}
func checkFDroidAPI(mirrorURL, packageName string, currentVersionCode int32) (*FDroidUpdateInfo, error) {
client := newFDroidHTTPClient()
defer client.CloseIdleConnections()
apiURL := "https://f-droid.org/api/v1/packages/" + packageName
request, err := newFDroidRequest(apiURL)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, E.New("HTTP ", response.Status)
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
var apiResponse fdroidAPIResponse
err = json.Unmarshal(body, &apiResponse)
if err != nil {
return nil, err
}
var bestCode int32
var bestName string
for _, pkg := range apiResponse.Packages {
if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode {
bestCode = pkg.VersionCode
bestName = pkg.VersionName
}
}
if bestCode == 0 {
return nil, nil
}
return &FDroidUpdateInfo{
VersionCode: bestCode,
VersionName: bestName,
DownloadURL: "https://f-droid.org/repo/" + packageName + "_" + strconv.FormatInt(int64(bestCode), 10) + ".apk",
}, nil
}
func checkFDroidV2(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) {
entryURL := mirrorURL + "/entry.jar"
request, err := newFDroidRequest(entryURL)
if err != nil {
return nil, err
}
if cache != nil && cache.ETag != "" {
request.Header.Set("If-None-Match", cache.ETag)
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotModified {
return nil, nil
}
if response.StatusCode == http.StatusNotFound {
writeFDroidCache(cachePath, mirrorURL, 0, "", true)
return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, nil)
}
if response.StatusCode != http.StatusOK {
return nil, E.New("HTTP ", response.Status, ": ", entryURL)
}
jarData, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
etag := response.Header.Get("ETag")
var entry fdroidEntry
err = readJSONFromJar(jarData, "entry.json", &entry)
if err != nil {
return nil, E.Cause(err, "read entry.jar")
}
if entry.Timestamp == 0 {
return nil, E.New("entry.json not found in entry.jar")
}
if cache != nil && cache.Timestamp == entry.Timestamp {
writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false)
return nil, nil
}
var indexURL string
if cache != nil {
cachedTimestamp := strconv.FormatInt(cache.Timestamp, 10)
if diff, ok := entry.Diffs[cachedTimestamp]; ok {
indexURL = mirrorURL + "/" + diff.Name
}
}
if indexURL == "" {
indexURL = mirrorURL + "/" + entry.Index.Name
}
indexRequest, err := newFDroidRequest(indexURL)
if err != nil {
return nil, err
}
indexResponse, err := client.Do(indexRequest)
if err != nil {
return nil, err
}
defer indexResponse.Body.Close()
if indexResponse.StatusCode != http.StatusOK {
return nil, E.New("HTTP ", indexResponse.Status, ": ", indexURL)
}
indexData, err := io.ReadAll(indexResponse.Body)
if err != nil {
return nil, err
}
var index fdroidIndexV2
err = json.Unmarshal(indexData, &index)
if err != nil {
return nil, err
}
writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false)
pkg, ok := index.Packages[packageName]
if !ok {
return nil, nil
}
var bestCode int32
var bestVersion fdroidV2Version
for _, version := range pkg.Versions {
if version.Manifest.VersionCode > currentVersionCode && version.Manifest.VersionCode > bestCode {
bestCode = version.Manifest.VersionCode
bestVersion = version
}
}
if bestCode == 0 {
return nil, nil
}
return &FDroidUpdateInfo{
VersionCode: bestCode,
VersionName: bestVersion.Manifest.VersionName,
DownloadURL: mirrorURL + "/" + bestVersion.File.Name,
FileSize: bestVersion.File.Size,
FileSHA256: bestVersion.File.SHA256,
}, nil
}
func checkFDroidV1(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) {
indexURL := mirrorURL + "/index-v1.jar"
request, err := newFDroidRequest(indexURL)
if err != nil {
return nil, err
}
if cache != nil && cache.ETag != "" {
request.Header.Set("If-None-Match", cache.ETag)
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotModified {
return nil, nil
}
if response.StatusCode != http.StatusOK {
return nil, E.New("HTTP ", response.Status, ": ", indexURL)
}
jarData, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
etag := response.Header.Get("ETag")
var index fdroidIndexV1
err = readJSONFromJar(jarData, "index-v1.json", &index)
if err != nil {
return nil, E.Cause(err, "read index-v1.jar")
}
writeFDroidCache(cachePath, mirrorURL, 0, etag, true)
packages, ok := index.Packages[packageName]
if !ok {
return nil, nil
}
var bestCode int32
var bestPackage fdroidV1Package
for _, pkg := range packages {
if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode {
bestCode = pkg.VersionCode
bestPackage = pkg
}
}
if bestCode == 0 {
return nil, nil
}
return &FDroidUpdateInfo{
VersionCode: bestCode,
VersionName: bestPackage.VersionName,
DownloadURL: mirrorURL + "/" + bestPackage.ApkName,
FileSize: bestPackage.Size,
FileSHA256: bestPackage.Hash,
}, nil
}
func readJSONFromJar(jarData []byte, fileName string, destination any) error {
zipReader, err := zip.NewReader(bytes.NewReader(jarData), int64(len(jarData)))
if err != nil {
return err
}
for _, file := range zipReader.File {
if file.Name != fileName {
continue
}
reader, err := file.Open()
if err != nil {
return err
}
data, err := io.ReadAll(reader)
reader.Close()
if err != nil {
return err
}
return json.Unmarshal(data, destination)
}
return nil
}
func pingTLS(mirrorURL string) (time.Duration, error) {
parsed, err := url.Parse(mirrorURL)
if err != nil {
return 0, err
}
host := parsed.Host
if !strings.Contains(host, ":") {
host = host + ":443"
}
dialer := &net.Dialer{Timeout: 5 * time.Second}
start := time.Now()
conn, err := tls.DialWithDialer(dialer, "tcp", host, &tls.Config{})
if err != nil {
return 0, err
}
latency := time.Since(start)
conn.Close()
return latency, nil
}
func loadFDroidCache(cachePath, mirrorURL string) *fdroidCache {
cacheFile := filepath.Join(cachePath, "fdroid_cache.json")
data, err := os.ReadFile(cacheFile)
if err != nil {
return nil
}
var cache fdroidCache
err = json.Unmarshal(data, &cache)
if err != nil {
return nil
}
if cache.MirrorURL != mirrorURL {
return nil
}
return &cache
}
func writeFDroidCache(cachePath, mirrorURL string, timestamp int64, etag string, isV1 bool) {
cache := fdroidCache{
MirrorURL: mirrorURL,
Timestamp: timestamp,
ETag: etag,
IsV1: isV1,
}
data, err := json.Marshal(cache)
if err != nil {
return
}
os.MkdirAll(cachePath, 0o755)
os.WriteFile(filepath.Join(cachePath, "fdroid_cache.json"), data, 0o644)
}

View File

@@ -0,0 +1,92 @@
package libbox
type FDroidMirror struct {
URL string
Country string
Name string
}
type FDroidMirrorIterator interface {
Len() int32
HasNext() bool
Next() *FDroidMirror
}
var builtinFDroidMirrors = []FDroidMirror{
// Official
{URL: "https://f-droid.org/repo", Country: "Official", Name: "f-droid.org"},
{URL: "https://cloudflare.f-droid.org/repo", Country: "Official", Name: "Cloudflare CDN"},
// China
{URL: "https://mirrors.tuna.tsinghua.edu.cn/fdroid/repo", Country: "China", Name: "Tsinghua TUNA"},
{URL: "https://mirrors.nju.edu.cn/fdroid/repo", Country: "China", Name: "Nanjing University"},
{URL: "https://mirror.iscas.ac.cn/fdroid/repo", Country: "China", Name: "ISCAS"},
{URL: "https://mirror.nyist.edu.cn/fdroid/repo", Country: "China", Name: "NYIST"},
{URL: "https://mirrors.cqupt.edu.cn/fdroid/repo", Country: "China", Name: "CQUPT"},
{URL: "https://mirrors.shanghaitech.edu.cn/fdroid/repo", Country: "China", Name: "ShanghaiTech"},
// India
{URL: "https://mirror.hyd.albony.in/fdroid/repo", Country: "India", Name: "Albony Hyderabad"},
{URL: "https://mirror.del2.albony.in/fdroid/repo", Country: "India", Name: "Albony Delhi"},
// Taiwan
{URL: "https://mirror.ossplanet.net/fdroid/repo", Country: "Taiwan", Name: "OSSPlanet"},
// France
{URL: "https://fdroid.tetaneutral.net/fdroid/repo", Country: "France", Name: "tetaneutral.net"},
{URL: "https://mirror.freedif.org/fdroid/repo", Country: "France", Name: "FreeDif"},
// Germany
{URL: "https://ftp.fau.de/fdroid/repo", Country: "Germany", Name: "FAU Erlangen"},
{URL: "https://ftp.agdsn.de/fdroid/repo", Country: "Germany", Name: "AGDSN Dresden"},
{URL: "https://ftp.gwdg.de/pub/android/fdroid/repo", Country: "Germany", Name: "GWDG"},
{URL: "https://mirror.level66.network/fdroid/repo", Country: "Germany", Name: "Level66"},
{URL: "https://mirror.mci-1.serverforge.org/fdroid/repo", Country: "Germany", Name: "ServerForge"},
// Netherlands
{URL: "https://ftp.snt.utwente.nl/pub/software/fdroid/repo", Country: "Netherlands", Name: "University of Twente"},
// Sweden
{URL: "https://ftp.lysator.liu.se/pub/fdroid/repo", Country: "Sweden", Name: "Lysator"},
// Denmark
{URL: "https://mirrors.dotsrc.org/fdroid/repo", Country: "Denmark", Name: "dotsrc.org"},
// Austria
{URL: "https://mirror.kumi.systems/fdroid/repo", Country: "Austria", Name: "Kumi Systems"},
// Switzerland
{URL: "https://mirror.init7.net/fdroid/repo", Country: "Switzerland", Name: "Init7"},
// Romania
{URL: "https://mirrors.hostico.ro/fdroid/repo", Country: "Romania", Name: "Hostico"},
{URL: "https://mirrors.chroot.ro/fdroid/repo", Country: "Romania", Name: "Chroot"},
{URL: "https://ftp.lug.ro/fdroid/repo", Country: "Romania", Name: "LUG Romania"},
// US
{URL: "https://plug-mirror.rcac.purdue.edu/fdroid/repo", Country: "US", Name: "Purdue"},
{URL: "https://mirror.fcix.net/fdroid/repo", Country: "US", Name: "FCIX"},
{URL: "https://opencolo.mm.fcix.net/fdroid/repo", Country: "US", Name: "OpenColo"},
{URL: "https://forksystems.mm.fcix.net/fdroid/repo", Country: "US", Name: "Fork Systems"},
{URL: "https://southfront.mm.fcix.net/fdroid/repo", Country: "US", Name: "South Front"},
{URL: "https://ziply.mm.fcix.net/fdroid/repo", Country: "US", Name: "Ziply"},
// Canada
{URL: "https://mirror.quantum5.ca/fdroid/repo", Country: "Canada", Name: "Quantum5"},
// Australia
{URL: "https://mirror.aarnet.edu.au/fdroid/repo", Country: "Australia", Name: "AARNet"},
// Other
{URL: "https://mirror.cyberbits.eu/fdroid/repo", Country: "Europe", Name: "Cyberbits EU"},
{URL: "https://mirror.eu.ossplanet.net/fdroid/repo", Country: "Europe", Name: "OSSPlanet EU"},
{URL: "https://mirror.cyberbits.asia/fdroid/repo", Country: "Asia", Name: "Cyberbits Asia"},
{URL: "https://mirrors.jevincanders.net/fdroid/repo", Country: "US", Name: "Jevincanders"},
{URL: "https://mirrors.komogoto.com/fdroid/repo", Country: "US", Name: "Komogoto"},
{URL: "https://fdroid.rasp.sh/fdroid/repo", Country: "Europe", Name: "rasp.sh"},
{URL: "https://mirror.gofoss.xyz/fdroid/repo", Country: "Europe", Name: "GoFOSS"},
}
func GetFDroidMirrors() FDroidMirrorIterator {
return newPtrIterator(builtinFDroidMirrors)
}

68
go.mod
View File

@@ -22,13 +22,13 @@ require (
github.com/metacubex/utls v1.8.4
github.com/mholt/acmez/v3 v3.1.6
github.com/miekg/dns v1.1.72
github.com/openai/openai-go/v3 v3.24.0
github.com/openai/openai-go/v3 v3.26.0
github.com/oschwald/maxminddb-golang v1.13.1
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1
github.com/sagernet/cronet-go v0.0.0-20260303101018-cba7b9ac0399
github.com/sagernet/cronet-go/all v0.0.0-20260303101018-cba7b9ac0399
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
@@ -39,10 +39,10 @@ require (
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.8.2
github.com/sagernet/sing-tun v0.8.3
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260303140313-3bcf9a4b9349
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.10.2
@@ -105,35 +105,35 @@ require (
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect

136
go.sum
View File

@@ -138,8 +138,8 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU=
github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE=
github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
@@ -162,68 +162,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/cronet-go v0.0.0-20260303101018-cba7b9ac0399 h1:x3tVYQHdqqnKbEd9/H4KIGhtHTjA+KfiiaXedI3/w8Q=
github.com/sagernet/cronet-go v0.0.0-20260303101018-cba7b9ac0399/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260303101018-cba7b9ac0399 h1:mD3ehudpYf1IFgCTv25d/B6KnBc/lLFq1jmSQIK24y0=
github.com/sagernet/cronet-go/all v0.0.0-20260303101018-cba7b9ac0399/go.mod h1:MbYagcGGIaRo9tNrgafbCTO+Qc7eVEh32ZWMprSB8b0=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260303100323-125d0d93b3e6 h1:ghRKgSaswefPwQF8AYtUlNyumILOB0ptJWxgZ8MFrEE=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:Behr7YCnQP2dsvzAJDIoMd5nTVU9/d6MMtk/S3MctwA=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260303100323-125d0d93b3e6 h1:6UL9XdGU/44oTHj36e+EBDJ0RonFoObmd299NG/qQCU=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:Q9apxjtkj6iMIBQlTo71QsOTrNlhHneaXQb1Q0IshU8=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:0N+xlnMkFEeqgFe3X/PEvHt+/t+BPgxmbx7wzNcYppg=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:7f2vTXtePikBSV1bdD0zs5/WuZM+bRuej3mREpWL/qQ=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:HMlnhEYs+axOa0tAJ79se3QsYB8CpRCQo9mewWWFeeg=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:Ux/U6vF+1AoGLSJK3jVa9Kqkn64MX4Ivv7fy0ikDrpQ=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:5Dhuere2bQFzfGvKxA7TFgA5MoTtgcZMmJQuKwQKlyA=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260303100323-125d0d93b3e6 h1:aMRcLow4UpZWZ28fR9FjveTL/4okrigZySIkEVZnlgA=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260303100323-125d0d93b3e6 h1:y4g8oNtEfSdcKrBKsH5vMAjzGthvhHFNU80sanYDQEM=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:CXN6OPILi5trwffmYiiJ9rqJL3XAWx1menLrBBwA0gU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:ZphFHQeFOTpqCWPwFcQRnrePXajml8LbKlYFJ5n0isU=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260303100323-125d0d93b3e6 h1:nKzFK84oANHz7I6bab+25bBY+pdpAbO0b3NJroyLldo=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:HqqZUGRXcWvvwlbuvjk/efo8TKW1H/aHdqQTde+Xs9Q=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:D2v9lZZG5sm4x/CkG7uqc6ZU3YlhFQ+GmJfvZMK0h/s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260303100323-125d0d93b3e6 h1:TWveNeXHrA5r8XOlf+vw7U2b2M0ip6GNF89jcUi1ogw=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260303100323-125d0d93b3e6 h1:DVCBoXOZI4PNG0cbCLg8lrphRXoLFcAIDLNmzsCVg3I=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:7s5xqNlBUWkIXdruPYi3/txXekQhGWxrYxbnB0cnARo=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260303100323-125d0d93b3e6 h1:eyEb+Q7VH4hpE1nV+EmEnN2XX5WilgBpIsfCw4C/7no=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260303100323-125d0d93b3e6 h1:9F1W7+z1hHST6GSzdpQ8Q0NCkneAL18dkRA1HfxH09A=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260303100323-125d0d93b3e6 h1:MmQIR3iJsdvw1ONBP3geK57i9c3+v9dXPMNdZYcYGKw=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260303100323-125d0d93b3e6 h1:j6Pk1Wsl+PCbKRXtp7a912D2D6zqX5Nk51wDQU9TEDc=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:0DnFhbRfNqwguNCxiinA7BowQ/RaFt627sjW09JNp80=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:3CZmlEk2/WW5UHLFJZxXPJ9IJxX3td8U3PyqWSGMl3c=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:eHkVRptoZf3BuuskkjcclO2dwQrX4zluoVGODMrX7n0=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:UgFmE0cZo9euu8/7sTAhj1G8lldavwXBdcPNyTE29CQ=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:xbg3ZB9tLMGDQe4+aewG0Z4bEP/2pLtYBcDzILv5eEc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:M0bTSTSTnSMlPY2WaZT6fL5TFICqk8v4cm+QVf8Fcao=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 h1:xq5Yr10jXEppD3cnGjE3WENaB6D0YsZu6KptZ8d3054=
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 h1:uxQyy6Y/boOuecVA66tf79JgtoRGfeDJcfYZZLKVA5E=
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:Xm6cCvs0/twozC1JYNq0sVlOVmcSGzV7YON1XGcD97w=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg=
@@ -248,14 +248,14 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.2 h1:rQr/x3eQCHh3oleIaoJdPdJwqzZp4+QWcJLT0Wz2xKY=
github.com/sagernet/sing-tun v0.8.2/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-tun v0.8.3 h1:mozxmuIoRhFdVHnheenLpBaammVj7bZPcnkApaYKDPY=
github.com/sagernet/sing-tun v0.8.3/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260303140313-3bcf9a4b9349 h1:ju7aTbndj2sqK4NplE97ynLdhuCtel5OS4e0NrT71nk=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260303140313-3bcf9a4b9349/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=

View File

@@ -1,3 +1,5 @@
//go:build with_gvisor
package tailscale
import (

View File

@@ -1,3 +1,5 @@
//go:build with_gvisor
package tailscale
import (
@@ -46,6 +48,7 @@ import (
"github.com/sagernet/tailscale/ipn"
tsDNS "github.com/sagernet/tailscale/net/dns"
"github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/net/netns"
"github.com/sagernet/tailscale/net/tsaddr"
tsTUN "github.com/sagernet/tailscale/net/tstun"
"github.com/sagernet/tailscale/tsnet"
@@ -108,6 +111,7 @@ type Endpoint struct {
systemInterfaceName string
systemInterfaceMTU uint32
systemTun tun.Tun
systemDialer *dialer.DefaultDialer
fallbackTCPCloser func()
}
@@ -144,7 +148,7 @@ func (t *Endpoint) registerNetstackHandlers() {
ctx := log.ContextWithNewID(t.ctx)
source := M.SocksaddrFrom(src.Addr(), src.Port())
destination := M.SocksaddrFrom(dst.Addr(), dst.Port())
packetConn := bufio.NewPacketConn(conn)
packetConn := bufio.NewUnbindPacketConnWithAddr(conn, destination)
t.NewPacketConnectionEx(ctx, packetConn, source, destination, nil)
}, true
}
@@ -285,9 +289,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
}
}), nil
})
if runtime.GOOS == "android" {
setAndroidProtectFunc(t.platformInterface)
}
}
if t.systemInterface {
mtu := t.systemInterfaceMTU
@@ -322,9 +323,30 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
_ = systemTun.Close()
return err
}
systemDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: tunName,
})
if err != nil {
_ = systemTun.Close()
return err
}
t.systemTun = systemTun
t.systemDialer = systemDialer
t.server.TunDevice = wgTunDevice
}
if mark := t.network.AutoRedirectOutputMark(); mark > 0 {
controlFunc := t.network.AutoRedirectOutputMarkFunc()
if bindFunc := t.network.AutoDetectInterfaceFunc(); bindFunc != nil {
controlFunc = control.Append(controlFunc, bindFunc)
}
netns.SetControlFunc(controlFunc)
} else if runtime.GOOS == "android" && t.platformInterface != nil {
netns.SetControlFunc(func(network, address string, c syscall.RawConn) error {
return control.Raw(c, func(fd uintptr) error {
return t.platformInterface.AutoDetectInterfaceControl(int(fd))
})
})
}
err := t.server.Start()
if err != nil {
if t.systemTun != nil {
@@ -450,14 +472,17 @@ func (t *Endpoint) watchState() {
func (t *Endpoint) Close() error {
netmon.RegisterInterfaceGetter(nil)
if runtime.GOOS == "android" {
setAndroidProtectFunc(nil)
}
netns.SetControlFunc(nil)
if t.fallbackTCPCloser != nil {
t.fallbackTCPCloser()
t.fallbackTCPCloser = nil
}
return common.Close(common.PtrOrNil(t.server))
err := common.Close(common.PtrOrNil(t.server))
if t.systemTun != nil {
t.systemTun.Close()
t.systemTun = nil
}
return err
}
func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
@@ -474,6 +499,9 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination
}
return N.DialSerial(ctx, t, network, destination, destinationAddresses)
}
if t.systemDialer != nil {
return t.systemDialer.DialContext(ctx, network, destination)
}
addr4, addr6 := t.server.TailscaleIPs()
remoteAddr := tcpip.FullAddress{
NIC: 1,
@@ -520,6 +548,9 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination
}
func (t *Endpoint) listenPacketWithAddress(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if t.systemDialer != nil {
return t.systemDialer.ListenPacket(ctx, destination)
}
addr4, addr6 := t.server.TailscaleIPs()
bind := tcpip.FullAddress{
NIC: 1,
@@ -677,19 +708,29 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
}
func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
inet4Address, inet6Address := t.server.TailscaleIPs()
if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() {
return nil, E.New("Tailscale is not ready yet")
}
ctx := log.ContextWithNewID(t.ctx)
destination, err := ping.ConnectGVisor(
ctx, t.logger,
metadata.Source.Addr, metadata.Destination.Addr,
routeContext,
t.stack,
inet4Address, inet6Address,
timeout,
)
var destination tun.DirectRouteDestination
var err error
if t.systemDialer != nil {
destination, err = ping.ConnectDestination(
ctx, t.logger,
t.systemDialer.DialerForICMPDestination(metadata.Destination.Addr).Control,
metadata.Destination.Addr, routeContext, timeout,
)
} else {
inet4Address, inet6Address := t.server.TailscaleIPs()
if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() {
return nil, E.New("Tailscale is not ready yet")
}
destination, err = ping.ConnectGVisor(
ctx, t.logger,
metadata.Source.Addr, metadata.Destination.Addr,
routeContext,
t.stack,
inet4Address, inet6Address,
timeout,
)
}
if err != nil {
return nil, err
}

View File

@@ -1,16 +0,0 @@
package tailscale
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/tailscale/net/netns"
)
func setAndroidProtectFunc(platformInterface adapter.PlatformInterface) {
if platformInterface != nil {
netns.SetAndroidProtectFunc(func(fd int) error {
return platformInterface.AutoDetectInterfaceControl(fd)
})
} else {
netns.SetAndroidProtectFunc(nil)
}
}

View File

@@ -1,8 +0,0 @@
//go:build !android
package tailscale
import "github.com/sagernet/sing-box/adapter"
func setAndroidProtectFunc(platformInterface adapter.PlatformInterface) {
}

View File

@@ -1,4 +1,4 @@
//go:build !windows
//go:build with_gvisor && !windows
package tailscale

View File

@@ -1,4 +1,4 @@
//go:build windows
//go:build with_gvisor && windows
package tailscale

View File

@@ -5,7 +5,9 @@
"dns": {
"servers": [
{
"address": "tls://8.8.8.8"
"type": "tls",
"tag": "google",
"server": "8.8.8.8"
}
]
},
@@ -26,17 +28,13 @@
"outbounds": [
{
"type": "direct"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"port": 53,
"outbound": "dns-out"
"action": "hijack-dns"
}
]
}

View File

@@ -0,0 +1,6 @@
# /etc/conf.d/sing-box: config file for /etc/init.d/sing-box
# sing-box configuration path, could be file or directory
# SINGBOX_CONFIG=/etc/sing-box
# SINGBOX_WORKDIR=/var/lib/sing-box

32
release/config/sing-box.initd Normal file → Executable file
View File

@@ -4,15 +4,41 @@ name=$RC_SVCNAME
description="sing-box service"
supervisor="supervise-daemon"
command="/usr/bin/sing-box"
command_args="-D /var/lib/sing-box -C /etc/sing-box run"
extra_commands="checkconfig"
extra_started_commands="reload"
: ${SINGBOX_CONFIG:=${config:-"/etc/sing-box"}}
if [ -d "$SINGBOX_CONFIG" ]; then
_config_opt="-C $SINGBOX_CONFIG"
elif [ -z "$SINGBOX_CONFIG" ]; then
_config_opt=""
else
_config_opt="-c $SINGBOX_CONFIG"
fi
_workdir=${SINGBOX_WORKDIR:-${workdir:-"/var/lib/sing-box"}}
command_args="run --disable-color
-D $_workdir
$_config_opt"
depend() {
after net dns
}
checkconfig() {
ebegin "Checking $RC_SVCNAME configuration"
sing-box check -D "$_workdir" $_config_opt
eend $?
}
start_pre() {
checkconfig
}
reload() {
ebegin "Reloading $RC_SVCNAME"
$supervisor "$RC_SVCNAME" --signal HUP
checkconfig && $supervisor "$RC_SVCNAME" --signal HUP
eend $?
}
}

View File

@@ -281,11 +281,11 @@ func (s *Service) getAccessToken() (string, error) {
return newCredentials.AccessToken, nil
}
func detectContextWindow(betaHeader string, inputTokens int64) int {
if inputTokens > premiumContextThreshold {
func detectContextWindow(betaHeader string, totalInputTokens int64) int {
if totalInputTokens > premiumContextThreshold {
features := strings.Split(betaHeader, ",")
for _, feature := range features {
if strings.TrimSpace(feature) == "context-1m" {
if strings.HasPrefix(strings.TrimSpace(feature), "context-1m") {
return contextWindowPremium
}
}
@@ -362,6 +362,13 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
serviceOverridesAcceptEncoding := len(s.httpHeaders.Values("Accept-Encoding")) > 0
if s.usageTracker != nil && !serviceOverridesAcceptEncoding {
// Strip Accept-Encoding so Go Transport adds it automatically
// and transparently decompresses the response for correct usage counting.
proxyRequest.Header.Del("Accept-Encoding")
}
anthropicBetaHeader := proxyRequest.Header.Get("anthropic-beta")
if anthropicBetaHeader != "" {
proxyRequest.Header.Set("anthropic-beta", anthropicBetaOAuthValue+","+anthropicBetaHeader)
@@ -447,7 +454,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
if usage.InputTokens > 0 || usage.OutputTokens > 0 {
if responseModel != "" {
contextWindow := detectContextWindow(anthropicBetaHeader, usage.InputTokens)
totalInputTokens := usage.InputTokens + usage.CacheCreationInputTokens + usage.CacheReadInputTokens
contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens)
s.usageTracker.AddUsageWithCycleHint(
responseModel,
contextWindow,
@@ -547,7 +555,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
if accumulatedUsage.InputTokens > 0 || accumulatedUsage.OutputTokens > 0 {
if responseModel != "" {
contextWindow := detectContextWindow(anthropicBetaHeader, accumulatedUsage.InputTokens)
totalInputTokens := accumulatedUsage.InputTokens + accumulatedUsage.CacheCreationInputTokens + accumulatedUsage.CacheReadInputTokens
contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens)
s.usageTracker.AddUsageWithCycleHint(
responseModel,
contextWindow,

View File

@@ -65,9 +65,10 @@ type CostCombinationJSON struct {
}
type CostsSummaryJSON struct {
TotalUSD float64 `json:"total_usd"`
ByUser map[string]float64 `json:"by_user"`
ByWeek map[string]float64 `json:"by_week,omitempty"`
TotalUSD float64 `json:"total_usd"`
ByUser map[string]float64 `json:"by_user"`
ByWeek map[string]float64 `json:"by_week,omitempty"`
ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"`
}
type AggregatedUsageJSON struct {
@@ -492,6 +493,31 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 {
return byWeek
}
func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 {
byUserAndWeek := make(map[string]map[string]float64)
for _, combination := range combinations {
if combination.WeekStartUnix <= 0 {
continue
}
weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
weekKey := formatWeekStartKey(weekStartAt)
for user, userStats := range combination.ByUser {
userWeeks, exists := byUserAndWeek[user]
if !exists {
userWeeks = make(map[string]float64)
byUserAndWeek[user] = userWeeks
}
userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow)
}
}
for _, weekCosts := range byUserAndWeek {
for weekKey, cost := range weekCosts {
weekCosts[weekKey] = roundCost(cost)
}
}
return byUserAndWeek
}
func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 {
if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() {
return 0
@@ -522,6 +548,11 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
result.Costs.ByWeek = nil
}
result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations)
if len(result.Costs.ByUserAndWeek) == 0 {
result.Costs.ByUserAndWeek = nil
}
for user, cost := range result.Costs.ByUser {
result.Costs.ByUser[user] = roundCost(cost)
}

View File

@@ -1,3 +1,5 @@
//go:build with_gvisor
package derp
import (

View File

@@ -130,6 +130,7 @@ type Service struct {
credentialPath string
credentials *oauthCredentials
users []option.OCMUser
dialer N.Dialer
httpClient *http.Client
httpHeaders http.Header
listener *listener.Listener
@@ -138,7 +139,9 @@ type Service struct {
userManager *UserManager
accessMutex sync.RWMutex
usageTracker *AggregatedUsage
trackingGroup sync.WaitGroup
webSocketMutex sync.Mutex
webSocketGroup sync.WaitGroup
webSocketConns map[*webSocketSession]struct{}
shuttingDown bool
}
@@ -187,6 +190,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
logger: logger,
credentialPath: options.CredentialPath,
users: options.Users,
dialer: serviceDialer,
httpClient: httpClient,
httpHeaders: options.Headers.Build(),
listener: listener.New(listener.Options{
@@ -195,8 +199,9 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
userManager: userManager,
usageTracker: usageTracker,
userManager: userManager,
usageTracker: usageTracker,
webSocketConns: make(map[*webSocketSession]struct{}),
}
if options.TLS != nil {
@@ -356,6 +361,11 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && strings.HasPrefix(path, "/v1/responses") {
s.handleWebSocket(w, r, proxyPath, username)
return
}
var requestModel string
if s.usageTracker != nil && r.Body != nil {
@@ -497,8 +507,10 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
responseModel = requestModel
}
if responseModel != "" {
contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens)
s.usageTracker.AddUsageWithCycleHint(
responseModel,
contextWindow,
inputTokens,
outputTokens,
cachedTokens,
@@ -606,8 +618,10 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
if inputTokens > 0 || outputTokens > 0 {
if responseModel != "" {
contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens)
s.usageTracker.AddUsageWithCycleHint(
responseModel,
contextWindow,
inputTokens,
outputTokens,
cachedTokens,
@@ -624,11 +638,17 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
}
func (s *Service) Close() error {
webSocketSessions := s.startWebSocketShutdown()
err := common.Close(
common.PtrOrNil(s.httpServer),
common.PtrOrNil(s.listener),
s.tlsConfig,
)
for _, session := range webSocketSessions {
session.Close()
}
s.webSocketGroup.Wait()
if s.usageTracker != nil {
s.usageTracker.cancelPendingSave()
@@ -640,3 +660,48 @@ func (s *Service) Close() error {
return err
}
func (s *Service) registerWebSocketSession(session *webSocketSession) bool {
s.webSocketMutex.Lock()
defer s.webSocketMutex.Unlock()
if s.shuttingDown {
return false
}
s.webSocketConns[session] = struct{}{}
s.webSocketGroup.Add(1)
return true
}
func (s *Service) unregisterWebSocketSession(session *webSocketSession) {
s.webSocketMutex.Lock()
_, loaded := s.webSocketConns[session]
if loaded {
delete(s.webSocketConns, session)
}
s.webSocketMutex.Unlock()
if loaded {
s.webSocketGroup.Done()
}
}
func (s *Service) isShuttingDown() bool {
s.webSocketMutex.Lock()
defer s.webSocketMutex.Unlock()
return s.shuttingDown
}
func (s *Service) startWebSocketShutdown() []*webSocketSession {
s.webSocketMutex.Lock()
defer s.webSocketMutex.Unlock()
s.shuttingDown = true
webSocketSessions := make([]*webSocketSession, 0, len(s.webSocketConns))
for session := range s.webSocketConns {
webSocketSessions = append(webSocketSessions, session)
}
return webSocketSessions
}

View File

@@ -46,6 +46,7 @@ func (u *UsageStats) UnmarshalJSON(data []byte) error {
type CostCombination struct {
Model string `json:"model"`
ServiceTier string `json:"service_tier,omitempty"`
ContextWindow int `json:"context_window"`
WeekStartUnix int64 `json:"week_start_unix,omitempty"`
Total UsageStats `json:"total"`
ByUser map[string]UsageStats `json:"by_user"`
@@ -74,15 +75,17 @@ type UsageStatsJSON struct {
type CostCombinationJSON struct {
Model string `json:"model"`
ServiceTier string `json:"service_tier,omitempty"`
ContextWindow int `json:"context_window"`
WeekStartUnix int64 `json:"week_start_unix,omitempty"`
Total UsageStatsJSON `json:"total"`
ByUser map[string]UsageStatsJSON `json:"by_user"`
}
type CostsSummaryJSON struct {
TotalUSD float64 `json:"total_usd"`
ByUser map[string]float64 `json:"by_user"`
ByWeek map[string]float64 `json:"by_week,omitempty"`
TotalUSD float64 `json:"total_usd"`
ByUser map[string]float64 `json:"by_user"`
ByWeek map[string]float64 `json:"by_week,omitempty"`
ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"`
}
type AggregatedUsageJSON struct {
@@ -103,8 +106,9 @@ type ModelPricing struct {
}
type modelFamily struct {
pattern *regexp.Regexp
pricing ModelPricing
pattern *regexp.Regexp
pricing ModelPricing
premiumPricing *ModelPricing
}
const (
@@ -115,6 +119,12 @@ const (
serviceTierScale = "scale"
)
const (
contextWindowStandard = 272000
contextWindowPremium = 1050000
premiumContextThreshold = 272000
)
var (
gpt52Pricing = ModelPricing{
InputPrice: 1.75,
@@ -158,6 +168,30 @@ var (
CachedInputPrice: 0.025,
}
gpt54StandardPricing = ModelPricing{
InputPrice: 2.5,
OutputPrice: 15.0,
CachedInputPrice: 0.25,
}
gpt54PremiumPricing = ModelPricing{
InputPrice: 5.0,
OutputPrice: 22.5,
CachedInputPrice: 0.5,
}
gpt54ProPricing = ModelPricing{
InputPrice: 30.0,
OutputPrice: 180.0,
CachedInputPrice: 30.0,
}
gpt54ProPremiumPricing = ModelPricing{
InputPrice: 60.0,
OutputPrice: 270.0,
CachedInputPrice: 60.0,
}
gpt52ProPricing = ModelPricing{
InputPrice: 21.0,
OutputPrice: 168.0,
@@ -170,6 +204,30 @@ var (
CachedInputPrice: 15.0,
}
gpt54FlexPricing = ModelPricing{
InputPrice: 1.25,
OutputPrice: 7.5,
CachedInputPrice: 0.125,
}
gpt54PremiumFlexPricing = ModelPricing{
InputPrice: 2.5,
OutputPrice: 11.25,
CachedInputPrice: 0.25,
}
gpt54ProFlexPricing = ModelPricing{
InputPrice: 15.0,
OutputPrice: 90.0,
CachedInputPrice: 15.0,
}
gpt54ProPremiumFlexPricing = ModelPricing{
InputPrice: 30.0,
OutputPrice: 135.0,
CachedInputPrice: 30.0,
}
gpt52FlexPricing = ModelPricing{
InputPrice: 0.875,
OutputPrice: 7.0,
@@ -194,6 +252,18 @@ var (
CachedInputPrice: 0.0025,
}
gpt54PriorityPricing = ModelPricing{
InputPrice: 5.0,
OutputPrice: 30.0,
CachedInputPrice: 0.5,
}
gpt54PremiumPriorityPricing = ModelPricing{
InputPrice: 10.0,
OutputPrice: 45.0,
CachedInputPrice: 1.0,
}
gpt52PriorityPricing = ModelPricing{
InputPrice: 3.5,
OutputPrice: 28.0,
@@ -381,6 +451,16 @@ var (
}
standardModelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`),
pricing: gpt54ProPricing,
premiumPricing: &gpt54ProPremiumPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
pricing: gpt54StandardPricing,
premiumPricing: &gpt54PremiumPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`),
pricing: gpt52CodexPricing,
@@ -524,6 +604,16 @@ var (
}
flexModelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`),
pricing: gpt54ProFlexPricing,
premiumPricing: &gpt54ProPremiumFlexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
pricing: gpt54FlexPricing,
premiumPricing: &gpt54PremiumFlexPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
pricing: gpt5MiniFlexPricing,
@@ -555,6 +645,11 @@ var (
}
priorityModelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
pricing: gpt54PriorityPricing,
premiumPricing: &gpt54PremiumPriorityPricing,
},
{
pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`),
pricing: gpt52CodexPriorityPricing,
@@ -637,15 +732,28 @@ func modelFamiliesForTier(serviceTier string) []modelFamily {
}
}
func findPricingInFamilies(model string, modelFamilies []modelFamily) (ModelPricing, bool) {
func findPricingInFamilies(model string, contextWindow int, modelFamilies []modelFamily) (ModelPricing, bool) {
isPremium := contextWindow >= contextWindowPremium
for _, family := range modelFamilies {
if family.pattern.MatchString(model) {
if isPremium && family.premiumPricing != nil {
return *family.premiumPricing, true
}
return family.pricing, true
}
}
return ModelPricing{}, false
}
func hasPremiumPricingInFamilies(model string, modelFamilies []modelFamily) bool {
for _, family := range modelFamilies {
if family.pattern.MatchString(model) {
return family.premiumPricing != nil
}
}
return false
}
func normalizeServiceTier(serviceTier string) string {
switch strings.ToLower(strings.TrimSpace(serviceTier)) {
case "", serviceTierAuto, serviceTierDefault:
@@ -662,27 +770,27 @@ func normalizeServiceTier(serviceTier string) string {
}
}
func getPricing(model string, serviceTier string) ModelPricing {
func getPricing(model string, serviceTier string, contextWindow int) ModelPricing {
normalizedServiceTier := normalizeServiceTier(serviceTier)
modelFamilies := modelFamiliesForTier(normalizedServiceTier)
families := modelFamiliesForTier(normalizedServiceTier)
if pricing, found := findPricingInFamilies(model, modelFamilies); found {
if pricing, found := findPricingInFamilies(model, contextWindow, families); found {
return pricing
}
normalizedModel := normalizeGPT5Model(model)
if normalizedModel != model {
if pricing, found := findPricingInFamilies(normalizedModel, modelFamilies); found {
if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, families); found {
return pricing
}
}
if normalizedServiceTier != serviceTierDefault {
if pricing, found := findPricingInFamilies(model, standardModelFamilies); found {
if pricing, found := findPricingInFamilies(model, contextWindow, standardModelFamilies); found {
return pricing
}
if normalizedModel != model {
if pricing, found := findPricingInFamilies(normalizedModel, standardModelFamilies); found {
if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, standardModelFamilies); found {
return pricing
}
}
@@ -691,6 +799,30 @@ func getPricing(model string, serviceTier string) ModelPricing {
return gpt4oPricing
}
func detectContextWindow(model string, serviceTier string, inputTokens int64) int {
if inputTokens <= premiumContextThreshold {
return contextWindowStandard
}
normalizedServiceTier := normalizeServiceTier(serviceTier)
families := modelFamiliesForTier(normalizedServiceTier)
if hasPremiumPricingInFamilies(model, families) {
return contextWindowPremium
}
normalizedModel := normalizeGPT5Model(model)
if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, families) {
return contextWindowPremium
}
if normalizedServiceTier != serviceTierDefault {
if hasPremiumPricingInFamilies(model, standardModelFamilies) {
return contextWindowPremium
}
if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, standardModelFamilies) {
return contextWindowPremium
}
}
return contextWindowStandard
}
func normalizeGPT5Model(model string) string {
if !strings.HasPrefix(model, "gpt-5.") {
return model
@@ -706,18 +838,18 @@ func normalizeGPT5Model(model string) string {
case strings.Contains(model, "-chat-latest"):
return "gpt-5.2-chat-latest"
case strings.Contains(model, "-pro"):
return "gpt-5.2-pro"
return "gpt-5.4-pro"
case strings.Contains(model, "-mini"):
return "gpt-5-mini"
case strings.Contains(model, "-nano"):
return "gpt-5-nano"
default:
return "gpt-5.2"
return "gpt-5.4"
}
}
func calculateCost(stats UsageStats, model string, serviceTier string) float64 {
pricing := getPricing(model, serviceTier)
func calculateCost(stats UsageStats, model string, serviceTier string, contextWindow int) float64 {
pricing := getPricing(model, serviceTier, contextWindow)
regularInputTokens := stats.InputTokens - stats.CachedTokens
if regularInputTokens < 0 {
@@ -738,13 +870,16 @@ func roundCost(cost float64) float64 {
func normalizeCombinations(combinations []CostCombination) {
for index := range combinations {
combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier)
if combinations[index].ContextWindow <= 0 {
combinations[index].ContextWindow = contextWindowStandard
}
if combinations[index].ByUser == nil {
combinations[index].ByUser = make(map[string]UsageStats)
}
}
}
func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) {
func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, contextWindow int, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) {
var matchedCombination *CostCombination
for index := range *combinations {
combination := &(*combinations)[index]
@@ -752,7 +887,7 @@ func addUsageToCombinations(combinations *[]CostCombination, model string, servi
if combination.ServiceTier != combinationServiceTier {
combination.ServiceTier = combinationServiceTier
}
if combination.Model == model && combinationServiceTier == serviceTier && combination.WeekStartUnix == weekStartUnix {
if combination.Model == model && combinationServiceTier == serviceTier && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix {
matchedCombination = combination
break
}
@@ -762,6 +897,7 @@ func addUsageToCombinations(combinations *[]CostCombination, model string, servi
newCombination := CostCombination{
Model: model,
ServiceTier: serviceTier,
ContextWindow: contextWindow,
WeekStartUnix: weekStartUnix,
Total: UsageStats{},
ByUser: make(map[string]UsageStats),
@@ -790,12 +926,13 @@ func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map
var totalCost float64
for index, combination := range combinations {
combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier)
combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow)
totalCost += combinationTotalCost
combinationJSON := CostCombinationJSON{
Model: combination.Model,
ServiceTier: combination.ServiceTier,
ContextWindow: combination.ContextWindow,
WeekStartUnix: combination.WeekStartUnix,
Total: UsageStatsJSON{
RequestCount: combination.Total.RequestCount,
@@ -808,7 +945,7 @@ func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map
}
for user, userStats := range combination.ByUser {
userCost := calculateCost(userStats, combination.Model, combination.ServiceTier)
userCost := calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow)
if aggregateUserCosts != nil {
aggregateUserCosts[user] += userCost
}
@@ -856,7 +993,7 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 {
}
weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
weekKey := formatWeekStartKey(weekStartAt)
byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier)
byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow)
}
for weekKey, weekCost := range byWeek {
byWeek[weekKey] = roundCost(weekCost)
@@ -864,6 +1001,31 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 {
return byWeek
}
func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 {
byUserAndWeek := make(map[string]map[string]float64)
for _, combination := range combinations {
if combination.WeekStartUnix <= 0 {
continue
}
weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
weekKey := formatWeekStartKey(weekStartAt)
for user, userStats := range combination.ByUser {
userWeeks, exists := byUserAndWeek[user]
if !exists {
userWeeks = make(map[string]float64)
byUserAndWeek[user] = userWeeks
}
userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow)
}
}
for _, weekCosts := range byUserAndWeek {
for weekKey, cost := range weekCosts {
weekCosts[weekKey] = roundCost(cost)
}
}
return byUserAndWeek
}
func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 {
if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() {
return 0
@@ -894,6 +1056,11 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
result.Costs.ByWeek = nil
}
result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations)
if len(result.Costs.ByUserAndWeek) == 0 {
result.Costs.ByUserAndWeek = nil
}
for user, cost := range result.Costs.ByUser {
result.Costs.ByUser[user] = roundCost(cost)
}
@@ -956,14 +1123,17 @@ func (u *AggregatedUsage) Save() error {
return err
}
func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error {
return u.AddUsageWithCycleHint(model, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil)
func (u *AggregatedUsage) AddUsage(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error {
return u.AddUsageWithCycleHint(model, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil)
}
func (u *AggregatedUsage) AddUsageWithCycleHint(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error {
func (u *AggregatedUsage) AddUsageWithCycleHint(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error {
if model == "" {
return E.New("model cannot be empty")
}
if contextWindow <= 0 {
return E.New("contextWindow must be positive")
}
normalizedServiceTier := normalizeServiceTier(serviceTier)
if observedAt.IsZero() {
@@ -976,7 +1146,7 @@ func (u *AggregatedUsage) AddUsageWithCycleHint(model string, inputTokens, outpu
u.LastUpdated = observedAt
weekStartUnix := deriveWeekStartUnix(cycleHint)
addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, weekStartUnix, user, inputTokens, outputTokens, cachedTokens)
addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens)
go u.scheduleSave()

View File

@@ -0,0 +1,285 @@
package ocm
import (
"context"
stdTLS "crypto/tls"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/ws"
"github.com/sagernet/ws/wsutil"
"github.com/openai/openai-go/v3/responses"
)
type webSocketSession struct {
clientConn net.Conn
upstreamConn net.Conn
closeOnce sync.Once
}
func (s *webSocketSession) Close() {
s.closeOnce.Do(func() {
s.clientConn.Close()
s.upstreamConn.Close()
})
}
func buildUpstreamWebSocketURL(baseURL string, proxyPath string) string {
upstreamURL := baseURL
if strings.HasPrefix(upstreamURL, "https://") {
upstreamURL = "wss://" + upstreamURL[len("https://"):]
} else if strings.HasPrefix(upstreamURL, "http://") {
upstreamURL = "ws://" + upstreamURL[len("http://"):]
}
return upstreamURL + proxyPath
}
func isForwardableResponseHeader(key string) bool {
lowerKey := strings.ToLower(key)
switch {
case strings.HasPrefix(lowerKey, "x-codex-"):
return true
case strings.HasPrefix(lowerKey, "x-reasoning"):
return true
case lowerKey == "openai-model":
return true
case strings.Contains(lowerKey, "-secondary-"):
return true
default:
return false
}
}
func isForwardableWebSocketRequestHeader(key string) bool {
if isHopByHopHeader(key) {
return false
}
lowerKey := strings.ToLower(key)
switch {
case lowerKey == "authorization":
return false
case strings.HasPrefix(lowerKey, "sec-websocket-"):
return false
default:
return true
}
}
func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyPath string, username string) {
accessToken, err := s.getAccessToken()
if err != nil {
s.logger.Error("get access token for websocket: ", err)
writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "authentication failed")
return
}
upstreamURL := buildUpstreamWebSocketURL(s.getBaseURL(), proxyPath)
if r.URL.RawQuery != "" {
upstreamURL += "?" + r.URL.RawQuery
}
upstreamHeaders := make(http.Header)
for key, values := range r.Header {
if isForwardableWebSocketRequestHeader(key) {
upstreamHeaders[key] = values
}
}
for key, values := range s.httpHeaders {
upstreamHeaders.Del(key)
upstreamHeaders[key] = values
}
upstreamHeaders.Set("Authorization", "Bearer "+accessToken)
if accountID := s.getAccountID(); accountID != "" {
upstreamHeaders.Set("ChatGPT-Account-Id", accountID)
}
upstreamResponseHeaders := make(http.Header)
upstreamDialer := ws.Dialer{
NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSConfig: &stdTLS.Config{
RootCAs: adapter.RootPoolFromContext(s.ctx),
Time: ntp.TimeFuncFromContext(s.ctx),
},
Header: ws.HandshakeHeaderHTTP(upstreamHeaders),
OnHeader: func(key, value []byte) error {
upstreamResponseHeaders.Add(string(key), string(value))
return nil
},
}
upstreamConn, upstreamBufferedReader, _, err := upstreamDialer.Dial(r.Context(), upstreamURL)
if err != nil {
s.logger.Error("dial upstream websocket: ", err)
writeJSONError(w, r, http.StatusBadGateway, "api_error", "upstream websocket connection failed")
return
}
weeklyCycleHint := extractWeeklyCycleHint(upstreamResponseHeaders)
clientResponseHeaders := make(http.Header)
for key, values := range upstreamResponseHeaders {
if isForwardableResponseHeader(key) {
clientResponseHeaders[key] = values
}
}
clientUpgrader := ws.HTTPUpgrader{
Header: clientResponseHeaders,
}
if s.isShuttingDown() {
upstreamConn.Close()
writeJSONError(w, r, http.StatusServiceUnavailable, "api_error", "service is shutting down")
return
}
clientConn, _, _, err := clientUpgrader.Upgrade(r, w)
if err != nil {
s.logger.Error("upgrade client websocket: ", err)
upstreamConn.Close()
return
}
session := &webSocketSession{
clientConn: clientConn,
upstreamConn: upstreamConn,
}
if !s.registerWebSocketSession(session) {
session.Close()
return
}
defer s.unregisterWebSocketSession(session)
var upstreamReadWriter io.ReadWriter
if upstreamBufferedReader != nil {
upstreamReadWriter = struct {
io.Reader
io.Writer
}{upstreamBufferedReader, upstreamConn}
} else {
upstreamReadWriter = upstreamConn
}
modelChannel := make(chan string, 1)
var waitGroup sync.WaitGroup
waitGroup.Add(2)
go func() {
defer waitGroup.Done()
defer session.Close()
s.proxyWebSocketClientToUpstream(clientConn, upstreamConn, modelChannel)
}()
go func() {
defer waitGroup.Done()
defer session.Close()
s.proxyWebSocketUpstreamToClient(upstreamReadWriter, clientConn, modelChannel, username, weeklyCycleHint)
}()
waitGroup.Wait()
}
func (s *Service) proxyWebSocketClientToUpstream(clientConn net.Conn, upstreamConn net.Conn, modelChannel chan<- string) {
for {
data, opCode, err := wsutil.ReadClientData(clientConn)
if err != nil {
if !E.IsClosedOrCanceled(err) {
s.logger.Debug("read client websocket: ", err)
}
return
}
if opCode == ws.OpText && s.usageTracker != nil {
var request struct {
Type string `json:"type"`
Model string `json:"model"`
}
if json.Unmarshal(data, &request) == nil && request.Type == "response.create" && request.Model != "" {
select {
case modelChannel <- request.Model:
default:
}
}
}
err = wsutil.WriteClientMessage(upstreamConn, opCode, data)
if err != nil {
if !E.IsClosedOrCanceled(err) {
s.logger.Debug("write upstream websocket: ", err)
}
return
}
}
}
func (s *Service) proxyWebSocketUpstreamToClient(upstreamReadWriter io.ReadWriter, clientConn net.Conn, modelChannel <-chan string, username string, weeklyCycleHint *WeeklyCycleHint) {
var requestModel string
for {
data, opCode, err := wsutil.ReadServerData(upstreamReadWriter)
if err != nil {
if !E.IsClosedOrCanceled(err) {
s.logger.Debug("read upstream websocket: ", err)
}
return
}
if opCode == ws.OpText && s.usageTracker != nil {
select {
case model := <-modelChannel:
requestModel = model
default:
}
var event struct {
Type string `json:"type"`
}
if json.Unmarshal(data, &event) == nil && event.Type == "response.completed" {
var streamEvent responses.ResponseStreamEventUnion
if json.Unmarshal(data, &streamEvent) == nil {
completedEvent := streamEvent.AsResponseCompleted()
responseModel := string(completedEvent.Response.Model)
serviceTier := string(completedEvent.Response.ServiceTier)
inputTokens := completedEvent.Response.Usage.InputTokens
outputTokens := completedEvent.Response.Usage.OutputTokens
cachedTokens := completedEvent.Response.Usage.InputTokensDetails.CachedTokens
if inputTokens > 0 || outputTokens > 0 {
if responseModel == "" {
responseModel = requestModel
}
if responseModel != "" {
contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens)
s.usageTracker.AddUsageWithCycleHint(
responseModel,
contextWindow,
inputTokens,
outputTokens,
cachedTokens,
serviceTier,
username,
time.Now(),
weeklyCycleHint,
)
}
}
}
}
}
err = wsutil.WriteServerMessage(clientConn, opCode, data)
if err != nil {
if !E.IsClosedOrCanceled(err) {
s.logger.Debug("write client websocket: ", err)
}
return
}
}
}