Compare commits

..

100 Commits

Author SHA1 Message Date
世界
275d102587 documentation: Bump version 2026-01-09 22:52:17 +08:00
世界
d553a761a9 Add dial option bind_address_no_port 2026-01-09 22:52:12 +08:00
世界
642719b3b6 platform: Display k based bytes 2026-01-09 22:52:05 +08:00
世界
c325c4ab51 Fix tailscale endpoint 2026-01-09 22:52:05 +08:00
世界
5842fde994 documentation: Bump version 2026-01-08 23:29:31 +08:00
世界
f45291fa79 tailscale: Add system interface support 2026-01-08 23:29:31 +08:00
世界
7f56a3ae8f platform: Fix gomobile build 2026-01-08 23:29:31 +08:00
世界
d5bb1a30dd platform: Update apple build comamnds 2026-01-08 23:29:31 +08:00
世界
4fbf0c4bd6 platform: Improve interface 2026-01-08 23:29:31 +08:00
世界
762b567693 documentation: Bump version 2026-01-07 15:16:51 +08:00
世界
1a511ae711 Downgrade quic-go to v0.57.1 2026-01-07 15:16:51 +08:00
世界
dcf5fde029 Fix linux musl builds 2026-01-07 15:16:51 +08:00
世界
04949914f1 Fix openwrt builds 2026-01-07 15:16:51 +08:00
世界
fbc0fe43ad Add kmod-nft-queue dependency for openwrt package 2026-01-07 15:16:51 +08:00
世界
857319ef3d Fix nfqueue fallback 2026-01-07 15:16:51 +08:00
世界
b48ddee8b1 Disable multipath TCP by default via GODEBUG 2026-01-07 15:16:51 +08:00
世界
8e3d70d197 Fix missing relay support for Tailscale 2026-01-07 15:16:51 +08:00
世界
21468cda93 cronet: Fix windows DNS hijack 2026-01-07 15:16:51 +08:00
世界
2ff67c9ece Fix DNS transports 2026-01-07 15:16:51 +08:00
世界
6ff279ba49 platform: Expose process info 2026-01-07 15:16:51 +08:00
世界
5d15a241e3 Update bypass action behavior for auto redirect 2026-01-07 15:16:51 +08:00
世界
e01a5c96a5 platform: Add GetStartedAt for StartedService 2026-01-07 15:16:51 +08:00
世界
9d40074cfa documentation: Format changes header 2026-01-07 15:16:51 +08:00
世界
4796bce0c2 Add format_docs command for documentation trailing space formatting 2026-01-07 15:16:51 +08:00
世界
fe632ef3ff Fix panic when closing Box before Start with file log output 2026-01-07 15:16:51 +08:00
世界
b15fecf992 Add pre-match support for auto redirect 2026-01-07 15:16:51 +08:00
世界
774269a2bf Fix cronet on iOS 2026-01-07 15:16:51 +08:00
世界
c995273244 Ignore darwin IP_DONTFRAG error when not supported 2026-01-07 15:15:29 +08:00
世界
a80208e7b1 Update tailscale to v1.92.4 2026-01-07 15:15:29 +08:00
世界
60580df446 Update cronet-go to v143.0.7499.109-1 2026-01-07 15:15:29 +08:00
世界
d28305be5f platform: Split library for Android SDK 21 and 23 2026-01-07 15:15:29 +08:00
世界
886a45a204 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2026-01-07 15:15:29 +08:00
世界
401d195944 documentation: Minor fixes 2026-01-07 15:15:29 +08:00
世界
d7489c0653 documentation: Add Wi-Fi state shared page 2026-01-07 15:15:29 +08:00
世界
1d6d829dee Fix missing build constraints for linux wifi state monitor 2026-01-07 15:15:28 +08:00
世界
69555fc6ea Update dependencies 2026-01-07 15:15:28 +08:00
世界
655e534ea8 Update quic-go to v0.58.0 2026-01-07 15:15:28 +08:00
世界
5d86f63906 Add Chrome Root Store certificate option
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
2026-01-07 15:15:28 +08:00
世界
2162dab4a1 Fix cronet-go crash 2026-01-07 15:15:28 +08:00
世界
51fd4b7e1a Add trace logging for lifecycle calls
Log start/close operations with timing information for debugging.
2026-01-07 15:15:28 +08:00
世界
ac51a74dbe documentation: Minor fixes 2026-01-07 15:15:28 +08:00
世界
d8a15ecddf Remove certificate_public_key_sha256 for naive 2026-01-07 15:15:28 +08:00
世界
dda91d50bd platform: Use new crash log api 2026-01-07 15:15:28 +08:00
世界
b3aec2997b Fix naive network 2026-01-07 15:15:27 +08:00
世界
f3ae84442a Add QUIC support for naiveproxy 2026-01-07 15:15:27 +08:00
世界
8d373fb004 Add ECH support for NaiveProxy outbound and tls.ech.query_server_name option
- Enable ECH for NaiveProxy outbound with DNS resolver integration
- Add query_server_name option to override domain for ECH HTTPS record queries
- Update cronet-go dependency and remove windows_386 support
2026-01-07 15:15:27 +08:00
世界
db3c3a6621 Fix naiveproxy build 2026-01-07 15:15:27 +08:00
世界
eaaaddb61e Add OpenAI Codex Multiplexer service 2026-01-07 15:15:27 +08:00
世界
e97784d05b Update pricing for CCM service 2026-01-07 15:15:27 +08:00
世界
7a0d211224 release: Upload only other apks 2026-01-07 15:15:26 +08:00
世界
5264802927 Fix bugs and add UoT option for naiveproxy outbound 2026-01-07 15:15:26 +08:00
世界
69896f8b80 Add naiveproxy outbound 2026-01-07 15:15:26 +08:00
世界
0e3552da6f Apply ping destination filter for Windows 2026-01-07 15:15:26 +08:00
世界
0ed29288a0 platform: Add UsePlatformWIFIMonitor to gRPC interface
Align dev-next-grpc with wip2 by adding UsePlatformWIFIMonitor()
to the new PlatformInterface, allowing platform clients to indicate
they handle WIFI monitoring themselves.
2026-01-07 15:15:26 +08:00
世界
774fed206b daemon: Add clear logs 2026-01-07 15:15:26 +08:00
世界
71e1eb1e7f Revert "Stop using DHCP on iOS and tvOS" 2026-01-07 15:15:25 +08:00
世界
5112488d39 platform: Refactoring libbox to use gRPC-based protocol 2026-01-07 15:15:25 +08:00
世界
f228bd5b49 Add Windows WI-FI state support 2026-01-07 15:15:25 +08:00
世界
6424476ace Add Linux WI-FI state support
Support monitoring WIFI state on Linux through:
- NetworkManager (D-Bus)
- IWD (D-Bus)
- wpa_supplicant (control socket)
- ConnMan (D-Bus)
2026-01-07 15:15:24 +08:00
世界
51dc7c4f88 Add more tcp keep alive options
Also update default TCP keep-alive initial period from 10 minutes to 5 minutes.
2026-01-07 15:15:24 +08:00
世界
92c072d849 Update quic-go to v0.57.1 2026-01-07 15:15:24 +08:00
世界
ac7f0c9a3d Fix read credentials for ccm service 2026-01-07 15:15:24 +08:00
世界
31dcdc4f14 Add claude code multiplexer service 2026-01-07 15:15:23 +08:00
世界
3a267e1557 Fix compatibility with MPTCP 2026-01-07 15:15:23 +08:00
世界
73c5575866 Use a more conservative strategy for resolving with systemd-resolved for local DNS server 2026-01-07 15:15:23 +08:00
世界
f00854dd72 Fix missing mTLS support in client options 2026-01-07 15:15:23 +08:00
世界
c04d1414dd Add curve preferences, pinned public key SHA256 and mTLS for TLS options 2026-01-07 15:15:23 +08:00
世界
ec4e411fa7 Fix WireGuard input packet 2026-01-07 15:15:23 +08:00
世界
cc3a4c7dec Update tfo-go to latest 2026-01-07 15:15:22 +08:00
世界
20b0e4c3a0 Remove compatibility codes 2026-01-07 15:15:22 +08:00
世界
5aabc42823 Do not use linkname by default to simplify debugging 2026-01-07 15:15:22 +08:00
世界
f0771fb623 documentation: Update chinese translations 2026-01-07 15:15:22 +08:00
世界
67c79ebac8 Update quic-go to v0.55.0 2026-01-07 15:15:22 +08:00
世界
ef185fed09 Update WireGuard and Tailscale 2026-01-07 15:15:22 +08:00
世界
f8cdc40d62 Fix preConnectionCopy 2026-01-07 15:15:22 +08:00
世界
3bcf072750 Fix ping domain 2026-01-07 15:15:22 +08:00
世界
3ef15fa8aa release: Fix linux build 2026-01-07 15:15:21 +08:00
世界
159418324a Improve ktls rx error handling 2026-01-07 15:15:21 +08:00
世界
0c30128c44 Improve compatibility for kTLS 2026-01-07 15:15:21 +08:00
世界
354330cd80 ktls: Add warning for inappropriate scenarios 2026-01-07 15:15:21 +08:00
世界
b991ae1c91 Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2026-01-07 15:15:21 +08:00
世界
53fef79ab3 Add proxy support for ICMP echo request 2026-01-07 15:15:21 +08:00
世界
dfe2e083d5 Fix resolve using resolved 2026-01-07 15:15:21 +08:00
世界
ce408b2989 documentation: Update behavior of local DNS server on darwin 2026-01-07 15:15:21 +08:00
世界
395a1aadfa Remove use of ldflags -checklinkname=0 on darwin 2026-01-07 15:15:20 +08:00
世界
905573e386 Fix legacy DNS config 2026-01-07 15:15:20 +08:00
世界
5814753603 Fix rule-set format 2026-01-07 15:15:20 +08:00
世界
8cef967847 documentation: Remove outdated icons 2026-01-07 15:15:20 +08:00
世界
af532b8d05 documentation: Improve local DNS server 2026-01-07 15:15:20 +08:00
世界
5585855367 Stop using DHCP on iOS and tvOS
We do not have the `com.apple.developer.networking.multicast` entitlement and are unable to obtain it for non-technical reasons.
2026-01-07 15:15:20 +08:00
世界
2344b35b0f Improve local DNS server on darwin
We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does.

This commit changes the behavior of the `local` DNS server in NetworkExtension to prefer DHCP, falling back to `getaddrinfo` if DHCP servers are unavailable.

It's worth noting that `prefer_go` does not disable DHCP since it respects Dial Fields, but `getaddrinfo` does the opposite. The new behavior only applies to NetworkExtension, not to all scenarios (primarily command-line binaries) as it did previously.

In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`.
2026-01-07 15:15:20 +08:00
世界
3a9ae0f91f Use resolved in local DNS server if available 2026-01-07 15:15:20 +08:00
xchacha20-poly1305
4fdbf786e1 Fix rule set version 2026-01-07 15:15:19 +08:00
世界
1e3ea29602 documentation: Add preferred_by route rule item 2026-01-07 15:15:19 +08:00
世界
ac5cb50368 Add preferred_by route rule item 2026-01-07 15:15:19 +08:00
世界
722f9ee971 documentation: Add interface address rule items 2026-01-07 15:15:19 +08:00
世界
3df72d04e9 Add interface address rule items 2026-01-07 15:15:19 +08:00
世界
4f93a515d6 Fix ECH retry support 2026-01-07 15:15:19 +08:00
neletor
a3c9ceca60 Add support for ech retry configs 2026-01-07 15:15:19 +08:00
Zephyruso
a57bdc82e8 Add /dns/flush-clash meta api 2026-01-07 15:15:18 +08:00
235 changed files with 3259 additions and 17772 deletions

View File

@@ -1,23 +0,0 @@
-s dir
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--config-files etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
LICENSE=/usr/share/licenses/sing-box/LICENSE

View File

@@ -1 +1 @@
ea7cd33752aed62603775af3df946c1b83f4b0b3
92d4602aba0ab6084673af0fe4887dccbc1049a5

View File

@@ -1,81 +0,0 @@
#!/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"

View File

@@ -1,80 +0,0 @@
#!/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"

View File

@@ -6,7 +6,7 @@
":disableRateLimiting"
],
"baseBranches": [
"unstable"
"dev-next"
],
"golang": {
"enabled": false

View File

@@ -1,45 +0,0 @@
#!/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,35 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.25.5"
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"
mkdir -p $HOME/go
cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_win7
cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# these patch URLs only work on golang1.25.x
# this patch file only works 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:
@@ -37,10 +18,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\""
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done
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

View File

@@ -10,4 +10,4 @@ git -C $PROJECTS/cronet-go fetch origin go
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
go mod tidy
git -C $PROJECTS/cronet-go rev-parse origin/go > "$SCRIPT_DIR/CRONET_GO_VERSION"
git -C $PROJECTS/cronet-go rev-parse origin/HEAD > "$SCRIPT_DIR/CRONET_GO_VERSION"

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
SCRIPT_DIR=$(dirname "$0")
PROJECTS=$SCRIPT_DIR/../..
git -C $PROJECTS/cronet-go fetch origin dev
git -C $PROJECTS/cronet-go fetch origin go_dev
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
go mod tidy
git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"

View File

@@ -25,9 +25,8 @@ on:
- publish-android
push:
branches:
- stable
- testing
- unstable
- main-next
- dev-next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
@@ -47,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -72,41 +71,33 @@ 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, alpine: x86_64, openwrt: "x86_64" }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: 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, alpine: 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, 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, alpine: x86, openwrt: "i386_pentium4" }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, 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, 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, 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, alpine: loongarch64, openwrt: "loongarch64_generic" }
- { 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: "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" }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
- { os: linux, arch: mipsle, gomips: hardfloat, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat }
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
- { os: linux, arch: mips64le, gomips: hardfloat }
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64 }
- { os: linux, arch: loong64 }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
@@ -121,10 +112,15 @@ jobs:
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! matrix.legacy_win7 }}
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.10
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
@@ -132,11 +128,9 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1258
key: go_win7_1255
- 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
@@ -160,23 +154,14 @@ jobs:
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
~/cronet-go/naiveproxy/src/out/sysroot-build
key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
@@ -205,10 +190,9 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS="${TAGS},with_naive_outbound"
fi
if [[ "${{ matrix.variant }}" == "purego" ]]; then
TAGS="${TAGS},with_purego"
@@ -216,16 +200,13 @@ jobs:
TAGS="${TAGS},with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build (purego)
if: matrix.variant == 'purego'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -247,7 +228,7 @@ jobs:
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
@@ -255,8 +236,6 @@ jobs:
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (musl)
if: matrix.variant == 'musl'
@@ -264,7 +243,7 @@ jobs:
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
@@ -272,8 +251,6 @@ jobs:
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-variant)
if: matrix.os != 'android' && matrix.variant == ''
@@ -281,7 +258,7 @@ jobs:
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -301,7 +278,7 @@ jobs:
export CXX="${CC}++"
mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
@@ -375,7 +352,7 @@ jobs:
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y libarchive-tools
cp .fpm_pacman .fpm
cp .fpm_systemd .fpm
fpm -t pacman \
-v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
@@ -396,30 +373,6 @@ 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
@@ -455,36 +408,22 @@ jobs:
include:
- { arch: amd64 }
- { arch: arm64 }
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! matrix.legacy_osx }}
if: ${{ ! matrix.legacy_go124 }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
- name: Cache Go for macOS 10.13
if: matrix.legacy_osx
id: cache-go-for-macos1013
uses: actions/cache@v4
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
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
go-version: ~1.24.6
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -492,27 +431,22 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
TAGS="${TAGS},with_naive_outbound"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
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: |-
@@ -565,11 +499,9 @@ jobs:
- name: Build
if: matrix.naive
run: |
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_WINDOWS
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
mkdir -p dist
go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
go build -v -trimpath -o dist/sing-box.exe -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0" `
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0" `
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -579,11 +511,9 @@ jobs:
- name: Build
if: ${{ !matrix.naive }}
run: |
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_OTHERS
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
mkdir -p dist
go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
go build -v -trimpath -o dist/sing-box.exe -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" `
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0" `
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -628,7 +558,7 @@ jobs:
path: "dist"
build_android:
name: Build Android
if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable'
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
runs-on: ubuntu-latest
needs:
- calculate_version
@@ -641,7 +571,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -664,12 +594,12 @@ jobs:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Checkout main branch
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
run: |-
cd clients/android
git checkout main
- name: Checkout dev branch
if: github.ref == 'refs/heads/testing'
if: github.ref == 'refs/heads/dev-next'
run: |-
cd clients/android
git checkout dev
@@ -718,7 +648,7 @@ jobs:
path: 'dist'
publish_android:
name: Publish Android
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable'
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
runs-on: ubuntu-latest
needs:
- calculate_version
@@ -731,7 +661,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -754,12 +684,12 @@ jobs:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Checkout main branch
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
run: |-
cd clients/android
git checkout main
- name: Checkout dev branch
if: github.ref == 'refs/heads/testing'
if: github.ref == 'refs/heads/dev-next'
run: |-
cd clients/android
git checkout dev
@@ -830,7 +760,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Set tag
if: matrix.if
run: |-
@@ -838,12 +768,12 @@ jobs:
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Checkout main branch
if: matrix.if && github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
run: |-
cd clients/apple
git checkout main
- name: Checkout dev branch
if: matrix.if && github.ref == 'refs/heads/testing'
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
cd clients/apple
git checkout dev
@@ -929,7 +859,7 @@ jobs:
-authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
- name: Publish to TestFlight
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing'
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
run: |-
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
- name: Build image

View File

@@ -3,8 +3,8 @@ name: Publish Docker Images
on:
#push:
# branches:
# - stable
# - testing
# - main-next
# - dev-next
release:
types:
- published
@@ -19,7 +19,6 @@ env:
jobs:
build_binary:
name: Build binary
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
strategy:
fail-fast: true
@@ -30,12 +29,10 @@ jobs:
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
- { arch: "386", naive: true, docker_platform: "linux/386" }
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
- { arch: mipsle, gomips: softfloat, naive: true, docker_platform: "linux/mipsle" }
- { arch: riscv64, naive: true, docker_platform: "linux/riscv64" }
- { arch: loong64, naive: true, docker_platform: "linux/loong64" }
# Non-naive builds
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
- { arch: riscv64, docker_platform: "linux/riscv64" }
- { arch: s390x, docker_platform: "linux/s390x" }
steps:
- name: Get commit to build
@@ -56,7 +53,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.4
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -67,23 +64,14 @@ jobs:
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
~/cronet-go/naiveproxy/src/out/sysroot-build
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
@@ -105,34 +93,29 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS="${TAGS},with_naive_outbound,with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build (naive)
if: matrix.naive
run: |
set -xeuo pipefail
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=${VERSION}\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: |
set -xeuo pipefail
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=${VERSION}\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "0"
@@ -165,17 +148,15 @@ jobs:
strategy:
fail-fast: true
matrix:
include:
- { platform: "linux/amd64" }
- { platform: "linux/arm/v6" }
- { platform: "linux/arm/v7" }
- { platform: "linux/arm64" }
- { platform: "linux/386" }
# mipsle: no base Docker image available for this platform
- { platform: "linux/ppc64le" }
- { platform: "linux/riscv64" }
- { platform: "linux/s390x" }
- { platform: "linux/loong64", base_image: "ghcr.io/loong64/alpine:edge" }
platform:
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64
- linux/386
- linux/ppc64le
- linux/riscv64
- linux/s390x
steps:
- name: Get commit to build
id: ref
@@ -228,8 +209,6 @@ jobs:
platforms: ${{ matrix.platform }}
context: .
file: Dockerfile.binary
build-args: |
BASE_IMAGE=${{ matrix.base_image || 'alpine' }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
@@ -245,7 +224,6 @@ jobs:
if-no-files-found: error
retention-days: 1
merge:
if: github.event_name != 'push'
runs-on: ubuntu-latest
needs:
- build_docker

View File

@@ -3,20 +3,18 @@ name: Lint
on:
push:
branches:
- oldstable
- stable
- testing
- unstable
- stable-next
- main-next
- dev-next
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/lint.yml'
pull_request:
branches:
- oldstable
- stable
- testing
- unstable
- stable-next
- main-next
- dev-next
jobs:
build:

View File

@@ -3,8 +3,8 @@ name: Build Linux Packages
on:
#push:
# branches:
# - stable
# - testing
# - main-next
# - dev-next
workflow_dispatch:
inputs:
version:
@@ -23,7 +23,6 @@ on:
jobs:
calculate_version:
name: Calculate version
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
@@ -35,7 +34,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -62,14 +61,14 @@ jobs:
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, debian: mipsel, rpm: mipsel }
- { os: linux, arch: riscv64, naive: true, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, naive: true, debian: loongarch64, rpm: loongarch64 }
# Non-naive builds (unsupported architectures)
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
@@ -78,7 +77,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.25.8
go-version: ^1.25.5
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -89,23 +88,14 @@ jobs:
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
~/cronet-go/naiveproxy/src/out/sysroot-build
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain
if: matrix.naive
@@ -126,30 +116,24 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS="${TAGS},with_naive_outbound,with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build (naive)
if: matrix.naive
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
@@ -157,7 +141,7 @@ jobs:
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"

3
.gitignore vendored
View File

@@ -12,9 +12,6 @@
/*.jar
/*.aar
/*.xcframework/
/experimental/libbox/*.aar
/experimental/libbox/*.xcframework/
/experimental/libbox/*.nupkg
.DS_Store
/config.d/
/venv/

View File

@@ -9,11 +9,6 @@ run:
- with_utls
- with_acme
- with_clash_api
- with_tailscale
- with_ccm
- with_ocm
- badlinkname
- tfogo_checklinkname0
linters:
default: none
enable:

View File

@@ -12,11 +12,10 @@ RUN set -ex \
&& apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
&& go build -v -trimpath -tags "$TAGS" \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" $LDFLAGS_SHARED -s -w -buildid=" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"

View File

@@ -1,14 +1,8 @@
ARG BASE_IMAGE=alpine
FROM ${BASE_IMAGE}
FROM alpine
ARG TARGETARCH
ARG TARGETVARIANT
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& if command -v apk > /dev/null; then \
apk add --no-cache --upgrade bash tzdata ca-certificates nftables; \
else \
apt-get update && apt-get install -y --no-install-recommends bash tzdata ca-certificates nftables \
&& rm -rf /var/lib/apt/lists/*; \
fi
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]

View File

@@ -1,18 +1,15 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
LDFLAGS_SHARED = $(shell cat release/LDFLAGS)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid="
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0"
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
.PHONY: test release docs build
@@ -92,12 +89,12 @@ update_android_version:
go run ./cmd/internal/update_android_version
build_android:
cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
upload_android:
mkdir -p dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android
@@ -209,12 +206,12 @@ update_apple_version:
update_macos_version:
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight:
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
go run -v ./cmd/internal/app_store_connect publish_testflight
prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store
@@ -237,21 +234,22 @@ test_stdio:
lib_android:
go run ./cmd/internal/build_libbox -target android
lib_android_debug:
go run ./cmd/internal/build_libbox -target android -debug
lib_apple:
go run ./cmd/internal/build_libbox -target apple
lib_windows:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
lib_ios:
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
lib_android_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
lib_apple_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
lib:
go run ./cmd/internal/build_libbox -target android
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.11
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.11
docs:
venv/bin/mkdocs serve
@@ -260,8 +258,8 @@ publish_docs:
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install:
python3 -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*"
python -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean:
rm -rf bin dist sing-box
@@ -271,6 +269,3 @@ update:
git fetch
git reset FETCH_HEAD --hard
git clean -fdx
%:
@:

View File

@@ -9,10 +9,6 @@ import (
type ConnectionManager interface {
Lifecycle
Count() int
CloseAll()
TrackConn(conn net.Conn) net.Conn
TrackPacketConn(conn net.PacketConn) net.PacketConn
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/binary"
"io"
"time"
"github.com/sagernet/sing/common/observable"
@@ -69,11 +68,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
if err != nil {
return nil, err
}
_, err = buffer.Write(s.Content)
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil {
return nil, err
}
@@ -81,11 +76,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
if err != nil {
return nil, err
}
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
if err != nil {
return nil, err
}
_, err = buffer.WriteString(s.LastEtag)
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil {
return nil, err
}
@@ -99,12 +90,7 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
if err != nil {
return err
}
contentLength, err := binary.ReadUvarint(reader)
if err != nil {
return err
}
s.Content = make([]byte, contentLength)
_, err = io.ReadFull(reader, s.Content)
err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
@@ -114,16 +100,10 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
return err
}
s.LastUpdated = time.Unix(lastUpdated, 0)
etagLength, err := binary.ReadUvarint(reader)
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil {
return err
}
etagBytes := make([]byte, etagLength)
_, err = io.ReadFull(reader, etagBytes)
if err != nil {
return err
}
s.LastEtag = string(etagBytes)
return nil
}

View File

@@ -2,7 +2,6 @@ package adapter
import (
"context"
"net"
"net/netip"
"time"
@@ -63,10 +62,13 @@ type InboundContext struct {
// cache
// Deprecated: implement in rule action
InboundDetour string
LastInbound string
OriginDestination M.Socksaddr
RouteOriginalDestination M.Socksaddr
InboundDetour string
LastInbound string
OriginDestination M.Socksaddr
RouteOriginalDestination M.Socksaddr
// Deprecated: to be removed
//nolint:staticcheck
InboundOptions option.InboundOptions
UDPDisableDomainUnmapping bool
UDPConnect bool
UDPTimeout time.Duration
@@ -83,8 +85,6 @@ type InboundContext struct {
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool

View File

@@ -1,23 +0,0 @@
package adapter
import (
"net"
"net/netip"
)
type NeighborEntry struct {
Address netip.Addr
MACAddress net.HardwareAddr
Hostname string
}
type NeighborResolver interface {
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
LookupHostname(address netip.Addr) (string, bool)
Start() error
Close() error
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries []NeighborEntry)
}

View File

@@ -36,10 +36,6 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type FindConnectionOwnerRequest struct {

View File

@@ -26,8 +26,6 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}

5
box.go
View File

@@ -125,10 +125,7 @@ func New(options Options) (*Box, error) {
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
if err != nil {
return nil, err
}
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needCacheFile bool
var needClashAPI bool
var needV2RayAPI bool

View File

@@ -100,32 +100,11 @@ findVersion:
}
func publishTestflight(ctx context.Context) error {
if len(os.Args) < 3 {
return E.New("platform required: ios, macos, or tvos")
}
var platform asc.Platform
switch os.Args[2] {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", os.Args[2])
}
tagVersion, err := build_shared.ReadTagVersion()
if err != nil {
return err
}
tag := tagVersion.VersionString()
releaseNotes := F.ToString("sing-box ", tagVersion.String())
if len(os.Args) >= 4 {
releaseNotes = strings.Join(os.Args[3:], " ")
}
client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs")
@@ -136,76 +115,97 @@ func publishTestflight(ctx context.Context) error {
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
var platforms []asc.Platform
if len(os.Args) == 3 {
switch os.Args[2] {
case "ios":
platforms = []asc.Platform{asc.PlatformIOS}
case "macos":
platforms = []asc.Platform{asc.PlatformMACOS}
case "tvos":
platforms = []asc.Platform{asc.PlatformTVOS}
default:
return E.New("unknown platform: ", os.Args[2])
}
} else {
platforms = []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
}
}
waitingForProcess := false
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
build := builds.Data[0]
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Fatal(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(releaseNotes))
for _, platform := range platforms {
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
build := builds.Data[0]
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
return err
}
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Fatal(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
F.ToString("sing-box ", tagVersion.String()),
))
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
}
return err
}
}
break
}
break
}
return nil
}

View File

@@ -17,17 +17,17 @@ import (
)
var (
debugEnabled bool
target string
platform string
// withTailscale bool
debugEnabled bool
target string
platform string
withTailscale bool
)
func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
// flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
}
func main() {
@@ -48,7 +48,7 @@ var (
debugFlags []string
sharedTags []string
darwinTags []string
// memcTags []string
memcTags []string
notMemcTags []string
debugTags []string
)
@@ -63,10 +63,9 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
// memcTags = append(memcTags, "with_tailscale")
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug")
}
@@ -165,7 +164,7 @@ func buildAndroid() {
// Build main variant (SDK 23)
mainTags := append([]string{}, sharedTags...)
// mainTags = append(mainTags, memcTags...)
mainTags = append(mainTags, memcTags...)
if debugEnabled {
mainTags = append(mainTags, debugTags...)
}
@@ -177,7 +176,7 @@ func buildAndroid() {
// Build legacy variant (SDK 21, no naive outbound)
legacyTags := filterTags(sharedTags, "with_naive_outbound")
// legacyTags = append(legacyTags, memcTags...)
legacyTags = append(legacyTags, memcTags...)
if debugEnabled {
legacyTags = append(legacyTags, debugTags...)
}
@@ -205,9 +204,9 @@ func buildApple() {
"-libname=box",
"-tags-not-macos=with_low_memory",
}
//if !withTailscale {
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
//}
if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
}
if !debugEnabled {
args = append(args, sharedFlags...)
@@ -216,9 +215,9 @@ func buildApple() {
}
tags := append(sharedTags, darwinTags...)
//if withTailscale {
// tags = append(tags, memcTags...)
//}
if withTailscale {
tags = append(tags, memcTags...)
}
if debugEnabled {
tags = append(tags, debugTags...)
}

View File

@@ -71,12 +71,12 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
version := strings.Trim(projectContent[versionStart:versionEnd], "\"")
version := projectContent[versionStart:versionEnd]
if version == newVersion {
continue
}
updated = true
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
}
return projectContent, updated
}

54
common/conntrack/conn.go Normal file
View File

@@ -0,0 +1,54 @@
package conntrack
import (
"io"
"net"
"github.com/sagernet/sing/common/x/list"
)
type Conn struct {
net.Conn
element *list.Element[io.Closer]
}
func NewConn(conn net.Conn) (net.Conn, error) {
connAccess.Lock()
element := openConnection.PushBack(conn)
connAccess.Unlock()
if KillerEnabled {
err := KillerCheck()
if err != nil {
conn.Close()
return nil, err
}
}
return &Conn{
Conn: conn,
element: element,
}, nil
}
func (c *Conn) Close() error {
if c.element.Value != nil {
connAccess.Lock()
if c.element.Value != nil {
openConnection.Remove(c.element)
c.element.Value = nil
}
connAccess.Unlock()
}
return c.Conn.Close()
}
func (c *Conn) Upstream() any {
return c.Conn
}
func (c *Conn) ReaderReplaceable() bool {
return true
}
func (c *Conn) WriterReplaceable() bool {
return true
}

View File

@@ -0,0 +1,35 @@
package conntrack
import (
runtimeDebug "runtime/debug"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
)
var (
KillerEnabled bool
MemoryLimit uint64
killerLastCheck time.Time
)
func KillerCheck() error {
if !KillerEnabled {
return nil
}
nowTime := time.Now()
if nowTime.Sub(killerLastCheck) < 3*time.Second {
return nil
}
killerLastCheck = nowTime
if memory.Total() > MemoryLimit {
Close()
go func() {
time.Sleep(time.Second)
runtimeDebug.FreeOSMemory()
}()
return E.New("out of memory")
}
return nil
}

View File

@@ -0,0 +1,55 @@
package conntrack
import (
"io"
"net"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/x/list"
)
type PacketConn struct {
net.PacketConn
element *list.Element[io.Closer]
}
func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) {
connAccess.Lock()
element := openConnection.PushBack(conn)
connAccess.Unlock()
if KillerEnabled {
err := KillerCheck()
if err != nil {
conn.Close()
return nil, err
}
}
return &PacketConn{
PacketConn: conn,
element: element,
}, nil
}
func (c *PacketConn) Close() error {
if c.element.Value != nil {
connAccess.Lock()
if c.element.Value != nil {
openConnection.Remove(c.element)
c.element.Value = nil
}
connAccess.Unlock()
}
return c.PacketConn.Close()
}
func (c *PacketConn) Upstream() any {
return bufio.NewPacketConn(c.PacketConn)
}
func (c *PacketConn) ReaderReplaceable() bool {
return true
}
func (c *PacketConn) WriterReplaceable() bool {
return true
}

47
common/conntrack/track.go Normal file
View File

@@ -0,0 +1,47 @@
package conntrack
import (
"io"
"sync"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/x/list"
)
var (
connAccess sync.RWMutex
openConnection list.List[io.Closer]
)
func Count() int {
if !Enabled {
return 0
}
return openConnection.Len()
}
func List() []io.Closer {
if !Enabled {
return nil
}
connAccess.RLock()
defer connAccess.RUnlock()
connList := make([]io.Closer, 0, openConnection.Len())
for element := openConnection.Front(); element != nil; element = element.Next() {
connList = append(connList, element.Value)
}
return connList
}
func Close() {
if !Enabled {
return
}
connAccess.Lock()
defer connAccess.Unlock()
for element := openConnection.Front(); element != nil; element = element.Next() {
common.Close(element.Value)
element.Value = nil
}
openConnection.Init()
}

View File

@@ -0,0 +1,5 @@
//go:build !with_conntrack
package conntrack
const Enabled = false

View File

@@ -0,0 +1,5 @@
//go:build with_conntrack
package conntrack
const Enabled = true

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
@@ -36,7 +37,6 @@ type DefaultDialer struct {
udpAddr4 string
udpAddr6 string
netns string
connectionManager adapter.ConnectionManager
networkManager adapter.NetworkManager
networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool
@@ -47,7 +47,6 @@ type DefaultDialer struct {
}
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
connectionManager := service.FromContext[adapter.ConnectionManager](ctx)
networkManager := service.FromContext[adapter.NetworkManager](ctx)
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
@@ -90,7 +89,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkManager != nil {
defaultOptions := networkManager.DefaultOptions()
if defaultOptions.BindInterface != "" && !disableDefaultBind {
if defaultOptions.BindInterface != "" {
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
@@ -158,11 +157,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if keepInterval == 0 {
keepInterval = C.TCPKeepAliveInterval
}
dialer.KeepAliveConfig = net.KeepAliveConfig{
Enable: true,
Idle: keepIdle,
Interval: keepInterval,
}
dialer.KeepAlive = keepIdle
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
}
var udpFragment bool
if options.UDPFragment != nil {
@@ -210,7 +206,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
udpAddr4: udpAddr4,
udpAddr6: udpAddr6,
netns: options.NetNs,
connectionManager: connectionManager,
networkManager: networkManager,
networkStrategy: networkStrategy,
defaultNetworkStrategy: defaultNetworkStrategy,
@@ -243,7 +238,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
return nil, E.New("domain not resolved")
}
if d.networkStrategy == nil {
return d.trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkUDP:
if !address.IsIPv6() {
@@ -308,12 +303,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
if !fastFallback && !isPrimary {
d.networkLastFallback.Store(time.Now())
}
return d.trackConn(conn, nil)
return trackConn(conn, nil)
}
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if d.networkStrategy == nil {
return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
if destination.IsIPv6() {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
@@ -365,23 +360,23 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
return nil, err
}
}
return d.trackPacketConn(packetConn, nil)
return trackPacketConn(packetConn, nil)
}
func (d *DefaultDialer) WireGuardControl() control.Func {
return d.udpListener.Control
}
func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) {
if d.connectionManager == nil || err != nil {
func trackConn(conn net.Conn, err error) (net.Conn, error) {
if !conntrack.Enabled || err != nil {
return conn, err
}
return d.connectionManager.TrackConn(conn), nil
return conntrack.NewConn(conn)
}
func (d *DefaultDialer) trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
if d.connectionManager == nil || err != nil {
func trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
if !conntrack.Enabled || err != nil {
return conn, err
}
return d.connectionManager.TrackPacketConn(conn), nil
return conntrack.NewPacketConn(conn)
}

View File

@@ -145,7 +145,3 @@ type ParallelNetworkDialer interface {
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
}
type PacketDialerWithDestination interface {
ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error)
}

View File

@@ -1,234 +0,0 @@
package geosite
import (
"bufio"
"bytes"
"encoding/binary"
"strings"
"testing"
"github.com/sagernet/sing/common/varbin"
"github.com/stretchr/testify/require"
)
// Old implementation using varbin reflection-based serialization
func oldWriteString(writer varbin.Writer, value string) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldWriteItem(writer varbin.Writer, item Item) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, item)
}
func oldReadString(reader varbin.Reader) (string, error) {
//nolint:staticcheck
return varbin.ReadValue[string](reader, binary.BigEndian)
}
func oldReadItem(reader varbin.Reader) (Item, error) {
//nolint:staticcheck
return varbin.ReadValue[Item](reader, binary.BigEndian)
}
func TestStringCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
}{
{"empty", ""},
{"single_char", "a"},
{"ascii", "example.com"},
{"utf8", "测试域名.中国"},
{"special_chars", "\x00\xff\n\t"},
{"127_bytes", strings.Repeat("x", 127)},
{"128_bytes", strings.Repeat("x", 128)},
{"16383_bytes", strings.Repeat("x", 16383)},
{"16384_bytes", strings.Repeat("x", 16384)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteString(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = writeString(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack2)
})
}
}
func TestItemCompat(t *testing.T) {
t.Parallel()
// Note: varbin.Write has a bug where struct values (not pointers) don't write their fields
// because field.CanSet() returns false for non-addressable values.
// The old geosite code passed Item values to varbin.Write, which silently wrote nothing.
// The new code correctly writes Type + Value using manual serialization.
// This test verifies the new serialization format and round-trip correctness.
cases := []struct {
name string
input Item
}{
{"domain_empty", Item{Type: RuleTypeDomain, Value: ""}},
{"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}},
{"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}},
{"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}},
{"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}},
{"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}},
{"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}},
{"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// New write
var newBuf bytes.Buffer
err := newBuf.WriteByte(byte(tc.input.Type))
require.NoError(t, err)
err = writeString(&newBuf, tc.input.Value)
require.NoError(t, err)
// Verify format: Type (1 byte) + Value (uvarint len + bytes)
require.True(t, len(newBuf.Bytes()) >= 1, "output too short")
require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch")
// New write -> old read (varbin can read correctly when given addressable target)
readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack)
// New write -> new read
reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes()))
typeByte, err := reader.ReadByte()
require.NoError(t, err)
value, err := readString(reader)
require.NoError(t, err)
require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value})
})
}
}
func TestGeositeWriteReadCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input map[string][]Item
}{
{
"empty_map",
map[string][]Item{},
},
{
"single_code_empty_items",
map[string][]Item{"test": {}},
},
{
"single_code_single_item",
map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}},
},
{
"single_code_multi_items",
map[string][]Item{
"test": {
{Type: RuleTypeDomain, Value: "a.com"},
{Type: RuleTypeDomainSuffix, Value: ".b.com"},
{Type: RuleTypeDomainKeyword, Value: "keyword"},
{Type: RuleTypeDomainRegex, Value: `^.*$`},
},
},
},
{
"multi_code",
map[string][]Item{
"cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}},
"us": {{Type: RuleTypeDomain, Value: "google.com"}},
"jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}},
},
},
{
"utf8_values",
map[string][]Item{
"test": {
{Type: RuleTypeDomain, Value: "测试.中国"},
{Type: RuleTypeDomainSuffix, Value: ".テスト"},
},
},
},
{
"large_items",
generateLargeItems(1000),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Write using new implementation
var buf bytes.Buffer
err := Write(&buf, tc.input)
require.NoError(t, err)
// Read back and verify
reader, codes, err := NewReader(bytes.NewReader(buf.Bytes()))
require.NoError(t, err)
// Verify all codes exist
codeSet := make(map[string]bool)
for _, code := range codes {
codeSet[code] = true
}
for code := range tc.input {
require.True(t, codeSet[code], "missing code: %s", code)
}
// Verify items match
for code, expectedItems := range tc.input {
items, err := reader.Read(code)
require.NoError(t, err)
require.Equal(t, expectedItems, items, "items mismatch for code: %s", code)
}
})
}
}
func generateLargeItems(count int) map[string][]Item {
items := make([]Item, count)
for i := 0; i < count; i++ {
items[i] = Item{
Type: ItemType(i % 4),
Value: strings.Repeat("x", i%200) + ".com",
}
}
return map[string][]Item{"large": items}
}

View File

@@ -9,6 +9,7 @@ import (
"sync/atomic"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
type Reader struct {
@@ -77,7 +78,7 @@ func (r *Reader) readMetadata() error {
codeIndex uint64
codeLength uint64
)
code, err = readString(reader)
code, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return err
}
@@ -111,16 +112,9 @@ func (r *Reader) Read(code string) ([]Item, error) {
}
r.bufferedReader.Reset(r.reader)
itemList := make([]Item, r.domainLength[code])
for i := range itemList {
typeByte, err := r.bufferedReader.ReadByte()
if err != nil {
return nil, err
}
itemList[i].Type = ItemType(typeByte)
itemList[i].Value, err = readString(r.bufferedReader)
if err != nil {
return nil, err
}
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
if err != nil {
return nil, err
}
return itemList, nil
}
@@ -141,18 +135,3 @@ func (r *readCounter) Read(p []byte) (n int, err error) {
}
return
}
func readString(reader io.ByteReader) (string, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return "", err
}
bytes := make([]byte, length)
for i := range bytes {
bytes[i], err = reader.ReadByte()
if err != nil {
return "", err
}
}
return string(bytes), nil
}

View File

@@ -2,6 +2,7 @@ package geosite
import (
"bytes"
"encoding/binary"
"sort"
"github.com/sagernet/sing/common/varbin"
@@ -19,11 +20,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
for _, code := range keys {
index[code] = content.Len()
for _, item := range domains[code] {
err := content.WriteByte(byte(item.Type))
if err != nil {
return err
}
err = writeString(content, item.Value)
err := varbin.Write(content, binary.BigEndian, item)
if err != nil {
return err
}
@@ -41,7 +38,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
}
for _, code := range keys {
err = writeString(writer, code)
err = varbin.Write(writer, binary.BigEndian, code)
if err != nil {
return err
}
@@ -62,12 +59,3 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
return nil
}
func writeString(writer varbin.Writer, value string) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
_, err = writer.Write([]byte(value))
return err
}

View File

@@ -151,7 +151,6 @@ 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

@@ -99,6 +99,8 @@ func (l *Listener) loopTCPIn() {
}
//nolint:staticcheck
metadata.InboundDetour = l.listenOptions.Detour
//nolint:staticcheck
metadata.InboundOptions = l.listenOptions.InboundOptions
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
ctx := log.ContextWithNewID(l.ctx)

View File

@@ -6,7 +6,6 @@ import (
"encoding/binary"
"io"
"net/netip"
"unsafe"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
@@ -506,24 +505,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
}
func readRuleItemString(reader varbin.Reader) ([]string, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
result := make([]string, length)
for i := range result {
strLen, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
buf := make([]byte, strLen)
_, err = io.ReadFull(reader, buf)
if err != nil {
return nil, err
}
result[i] = string(buf)
}
return result, nil
return varbin.ReadValue[[]string](reader, binary.BigEndian)
}
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
@@ -531,34 +513,11 @@ func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) e
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
for _, s := range value {
_, err = varbin.WriteUvarint(writer, uint64(len(s)))
if err != nil {
return err
}
_, err = writer.Write([]byte(s))
if err != nil {
return err
}
}
return nil
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
result := make([]E, length)
_, err = io.ReadFull(reader, *(*[]byte)(unsafe.Pointer(&result)))
if err != nil {
return nil, err
}
return result, nil
return varbin.ReadValue[[]E](reader, binary.BigEndian)
}
func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error {
@@ -566,25 +525,11 @@ func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
return err
return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
result := make([]uint16, length)
err = binary.Read(reader, binary.BigEndian, result)
if err != nil {
return nil, err
}
return result, nil
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
@@ -592,11 +537,7 @@ func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) e
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, value)
return varbin.Write(writer, binary.BigEndian, value)
}
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {

View File

@@ -1,494 +0,0 @@
package srs
import (
"bufio"
"bytes"
"encoding/binary"
"net/netip"
"strings"
"testing"
"unsafe"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"github.com/stretchr/testify/require"
"go4.org/netipx"
)
// Old implementations using varbin reflection-based serialization
func oldWriteStringSlice(writer varbin.Writer, value []string) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldReadStringSlice(reader varbin.Reader) ([]string, error) {
//nolint:staticcheck
return varbin.ReadValue[[]string](reader, binary.BigEndian)
}
func oldWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldReadUint8Slice[E ~uint8](reader varbin.Reader) ([]E, error) {
//nolint:staticcheck
return varbin.ReadValue[[]E](reader, binary.BigEndian)
}
func oldWriteUint16Slice(writer varbin.Writer, value []uint16) error {
//nolint:staticcheck
return varbin.Write(writer, binary.BigEndian, value)
}
func oldReadUint16Slice(reader varbin.Reader) ([]uint16, error) {
//nolint:staticcheck
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func oldWritePrefix(writer varbin.Writer, prefix netip.Prefix) error {
//nolint:staticcheck
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
}
type oldIPRangeData struct {
From []byte
To []byte
}
// Note: The old writeIPSet had a bug where varbin.Write(writer, binary.BigEndian, data)
// with a struct VALUE (not pointer) silently wrote nothing because field.CanSet() returned false.
// This caused IP range data to be missing from the output.
// The new implementation correctly writes all range data.
//
// The old readIPSet used varbin.Read with a pre-allocated slice, which worked because
// slice elements are addressable and CanSet() returns true for them.
//
// For compatibility testing, we verify:
// 1. New write produces correct output with range data
// 2. New read can parse the new format correctly
// 3. Round-trip works correctly
func oldReadIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil {
return nil, err
}
if version != 1 {
return nil, err
}
var length uint64
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
ranges := make([]oldIPRangeData, length)
//nolint:staticcheck
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)),
}
for i, rangeData := range ranges {
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
// New write functions (without itemType prefix for testing)
func newWriteStringSlice(writer varbin.Writer, value []string) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
for _, s := range value {
_, err = varbin.WriteUvarint(writer, uint64(len(s)))
if err != nil {
return err
}
_, err = writer.Write([]byte(s))
if err != nil {
return err
}
}
return nil
}
func newWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
return err
}
func newWriteUint16Slice(writer varbin.Writer, value []uint16) error {
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
if err != nil {
return err
}
return binary.Write(writer, binary.BigEndian, value)
}
func newWritePrefix(writer varbin.Writer, prefix netip.Prefix) error {
addrSlice := prefix.Addr().AsSlice()
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
if err != nil {
return err
}
_, err = writer.Write(addrSlice)
if err != nil {
return err
}
return writer.WriteByte(uint8(prefix.Bits()))
}
// Tests
func TestStringSliceCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input []string
}{
{"nil", nil},
{"empty", []string{}},
{"single_empty", []string{""}},
{"single", []string{"test"}},
{"multi", []string{"a", "b", "c"}},
{"with_empty", []string{"a", "", "c"}},
{"utf8", []string{"测试", "テスト", "тест"}},
{"long_string", []string{strings.Repeat("x", 128)}},
{"many_elements", generateStrings(128)},
{"many_elements_256", generateStrings(256)},
{"127_byte_string", []string{strings.Repeat("x", 127)}},
{"128_byte_string", []string{strings.Repeat("x", 128)}},
{"mixed_lengths", []string{"a", strings.Repeat("b", 100), "", strings.Repeat("c", 200)}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteStringSlice(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWriteStringSlice(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadStringSlice(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireStringSliceEqual(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readRuleItemString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
requireStringSliceEqual(t, tc.input, readBack2)
})
}
}
func TestUint8SliceCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input []uint8
}{
{"nil", nil},
{"empty", []uint8{}},
{"single_zero", []uint8{0}},
{"single_max", []uint8{255}},
{"multi", []uint8{0, 1, 127, 128, 255}},
{"boundary", []uint8{0x00, 0x7f, 0x80, 0xff}},
{"sequential", generateUint8Slice(256)},
{"127_elements", generateUint8Slice(127)},
{"128_elements", generateUint8Slice(128)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteUint8Slice(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWriteUint8Slice(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadUint8Slice[uint8](bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireUint8SliceEqual(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readRuleItemUint8[uint8](bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
requireUint8SliceEqual(t, tc.input, readBack2)
})
}
}
func TestUint16SliceCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input []uint16
}{
{"nil", nil},
{"empty", []uint16{}},
{"single_zero", []uint16{0}},
{"single_max", []uint16{65535}},
{"multi", []uint16{0, 255, 256, 32767, 32768, 65535}},
{"ports", []uint16{80, 443, 8080, 8443}},
{"127_elements", generateUint16Slice(127)},
{"128_elements", generateUint16Slice(128)},
{"256_elements", generateUint16Slice(256)},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWriteUint16Slice(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWriteUint16Slice(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> old read
readBack, err := oldReadUint16Slice(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireUint16SliceEqual(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readRuleItemUint16(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
requireUint16SliceEqual(t, tc.input, readBack2)
})
}
}
func TestPrefixCompat(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input netip.Prefix
}{
{"ipv4_0", netip.MustParsePrefix("0.0.0.0/0")},
{"ipv4_8", netip.MustParsePrefix("10.0.0.0/8")},
{"ipv4_16", netip.MustParsePrefix("192.168.0.0/16")},
{"ipv4_24", netip.MustParsePrefix("192.168.1.0/24")},
{"ipv4_32", netip.MustParsePrefix("1.2.3.4/32")},
{"ipv6_0", netip.MustParsePrefix("::/0")},
{"ipv6_64", netip.MustParsePrefix("2001:db8::/64")},
{"ipv6_128", netip.MustParsePrefix("::1/128")},
{"ipv6_full", netip.MustParsePrefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")},
{"ipv4_private", netip.MustParsePrefix("172.16.0.0/12")},
{"ipv6_link_local", netip.MustParsePrefix("fe80::/10")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Old write
var oldBuf bytes.Buffer
err := oldWritePrefix(&oldBuf, tc.input)
require.NoError(t, err)
// New write
var newBuf bytes.Buffer
err = newWritePrefix(&newBuf, tc.input)
require.NoError(t, err)
// Bytes must match
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
// New write -> new read (no old read for prefix)
readBack, err := readPrefix(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack)
// Old write -> new read
readBack2, err := readPrefix(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
require.NoError(t, err)
require.Equal(t, tc.input, readBack2)
})
}
}
func TestIPSetCompat(t *testing.T) {
t.Parallel()
// Note: The old writeIPSet was buggy (varbin.Write with struct values wrote nothing).
// This test verifies the new implementation writes correct data and round-trips correctly.
cases := []struct {
name string
input *netipx.IPSet
}{
{"single_ipv4", buildIPSet("1.2.3.4")},
{"ipv4_range", buildIPSet("192.168.0.0/16")},
{"multi_ipv4", buildIPSet("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")},
{"single_ipv6", buildIPSet("::1")},
{"ipv6_range", buildIPSet("2001:db8::/32")},
{"mixed", buildIPSet("10.0.0.0/8", "::1", "2001:db8::/32")},
{"large", buildLargeIPSet(100)},
{"adjacent_ranges", buildIPSet("192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// New write
var newBuf bytes.Buffer
err := writeIPSet(&newBuf, tc.input)
require.NoError(t, err)
// Verify format starts with version byte (1) + uint64 count
require.True(t, len(newBuf.Bytes()) >= 9, "output too short")
require.Equal(t, byte(1), newBuf.Bytes()[0], "version byte mismatch")
// New write -> old read (varbin.Read with pre-allocated slice works correctly)
readBack, err := oldReadIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireIPSetEqual(t, tc.input, readBack)
// New write -> new read
readBack2, err := readIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
require.NoError(t, err)
requireIPSetEqual(t, tc.input, readBack2)
})
}
}
// Helper functions
func generateStrings(count int) []string {
result := make([]string, count)
for i := range result {
result[i] = strings.Repeat("x", i%50)
}
return result
}
func generateUint8Slice(count int) []uint8 {
result := make([]uint8, count)
for i := range result {
result[i] = uint8(i % 256)
}
return result
}
func generateUint16Slice(count int) []uint16 {
result := make([]uint16, count)
for i := range result {
result[i] = uint16(i * 257)
}
return result
}
func buildIPSet(cidrs ...string) *netipx.IPSet {
var builder netipx.IPSetBuilder
for _, cidr := range cidrs {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
addr, err := netip.ParseAddr(cidr)
if err != nil {
panic(err)
}
builder.Add(addr)
} else {
builder.AddPrefix(prefix)
}
}
set, _ := builder.IPSet()
return set
}
func buildLargeIPSet(count int) *netipx.IPSet {
var builder netipx.IPSetBuilder
for i := 0; i < count; i++ {
prefix := netip.PrefixFrom(netip.AddrFrom4([4]byte{10, byte(i / 256), byte(i % 256), 0}), 24)
builder.AddPrefix(prefix)
}
set, _ := builder.IPSet()
return set
}
func requireStringSliceEqual(t *testing.T, expected, actual []string) {
t.Helper()
if len(expected) == 0 && len(actual) == 0 {
return
}
require.Equal(t, expected, actual)
}
func requireUint8SliceEqual(t *testing.T, expected, actual []uint8) {
t.Helper()
if len(expected) == 0 && len(actual) == 0 {
return
}
require.Equal(t, expected, actual)
}
func requireUint16SliceEqual(t *testing.T, expected, actual []uint16) {
t.Helper()
if len(expected) == 0 && len(actual) == 0 {
return
}
require.Equal(t, expected, actual)
}
func requireIPSetEqual(t *testing.T, expected, actual *netipx.IPSet) {
t.Helper()
expectedRanges := expected.Ranges()
actualRanges := actual.Ranges()
require.Equal(t, len(expectedRanges), len(actualRanges), "range count mismatch")
for i := range expectedRanges {
require.Equal(t, expectedRanges[i].From(), actualRanges[i].From(), "range[%d].from mismatch", i)
require.Equal(t, expectedRanges[i].To(), actualRanges[i].To(), "range[%d].to mismatch", i)
}
}

View File

@@ -2,7 +2,6 @@ package srs
import (
"encoding/binary"
"io"
"net/netip"
M "github.com/sagernet/sing/common/metadata"
@@ -10,16 +9,11 @@ import (
)
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
addrLen, err := binary.ReadUvarint(reader)
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
addrSlice := make([]byte, addrLen)
_, err = io.ReadFull(reader, addrSlice)
if err != nil {
return netip.Prefix{}, err
}
prefixBits, err := reader.ReadByte()
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
@@ -27,16 +21,11 @@ func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
}
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
addrSlice := prefix.Addr().AsSlice()
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
_, err = writer.Write(addrSlice)
if err != nil {
return err
}
err = writer.WriteByte(uint8(prefix.Bits()))
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
if err != nil {
return err
}

View File

@@ -2,11 +2,11 @@ package srs
import (
"encoding/binary"
"io"
"net/netip"
"os"
"unsafe"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
@@ -22,6 +22,11 @@ type myIPRange struct {
to netip.Addr
}
type myIPRangeData struct {
From []byte
To []byte
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil {
@@ -36,30 +41,17 @@ func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
if err != nil {
return nil, err
}
mySet := &myIPSet{
rr: make([]myIPRange, length),
ranges := make([]myIPRangeData, length)
err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
}
for i := range mySet.rr {
fromLen, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
toLen, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
mySet.rr[i].from = M.AddrFromIP(fromBytes)
mySet.rr[i].to = M.AddrFromIP(toBytes)
mySet := &myIPSet{
rr: make([]myIPRange, len(ranges)),
}
for i, rangeData := range ranges {
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
}
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
}
@@ -69,27 +61,18 @@ func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
if err != nil {
return err
}
mySet := (*myIPSet)(unsafe.Pointer(set))
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
return myIPRangeData{
From: rr.from.AsSlice(),
To: rr.to.AsSlice(),
}
})
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
if err != nil {
return err
}
for _, rr := range mySet.rr {
fromBytes := rr.from.AsSlice()
_, err = varbin.WriteUvarint(writer, uint64(len(fromBytes)))
if err != nil {
return err
}
_, err = writer.Write(fromBytes)
if err != nil {
return err
}
toBytes := rr.to.AsSlice()
_, err = varbin.WriteUvarint(writer, uint64(len(toBytes)))
if err != nil {
return err
}
_, err = writer.Write(toBytes)
for _, data := range dataList {
err = varbin.Write(writer, binary.BigEndian, data)
if err != nil {
return err
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic"
"github.com/libdns/acmedns"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/v3/acme"
@@ -127,13 +126,6 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
APIToken: dnsOptions.CloudflareOptions.APIToken,
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
}
case C.DNSProviderACMEDNS:
solver.DNSProvider = &acmedns.Provider{
Username: dnsOptions.ACMEDNSOptions.Username,
Password: dnsOptions.ACMEDNSOptions.Password,
Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
}
default:
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
aTLS "github.com/sagernet/sing/common/tls"
@@ -37,7 +38,7 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op
}
//nolint:staticcheck
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
return nil, E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0")
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
}
if len(echConfig) > 0 {
block, rest := pem.Decode(echConfig)
@@ -76,7 +77,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
tlsConfig.EncryptedClientHelloKeys = echKeys
//nolint:staticcheck
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
return E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0")
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
}
return nil
}

View File

@@ -33,5 +33,4 @@ const (
const (
DNSProviderAliDNS = "alidns"
DNSProviderCloudflare = "cloudflare"
DNSProviderACMEDNS = "acmedns"
)

View File

@@ -30,7 +30,6 @@ const (
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
)
const (
@@ -86,8 +85,6 @@ func ProxyDisplayName(proxyType string) string {
return "Hysteria2"
case TypeAnyTLS:
return "AnyTLS"
case TypeTailscale:
return "Tailscale"
case TypeSelector:
return "Selector"
case TypeURLTest:

View File

@@ -7,12 +7,9 @@ import (
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service"
@@ -23,7 +20,6 @@ type Instance struct {
ctx context.Context
cancel context.CancelFunc
instance *box.Box
connectionManager adapter.ConnectionManager
clashServer adapter.ClashServer
cacheFile adapter.CacheFile
pauseManager pause.Manager
@@ -87,15 +83,6 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
}
}
}
if s.oomKiller && C.IsIos {
if !common.Any(options.Services, func(it option.Service) bool {
return it.Type == C.TypeOOMKiller
}) {
options.Services = append(options.Services, option.Service{
Type: C.TypeOOMKiller,
})
}
}
urlTestHistoryStorage := urltest.NewHistoryStorage()
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
i := &Instance{
@@ -113,11 +100,9 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
return nil, err
}
i.instance = boxInstance
i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx)
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
i.pauseManager = service.FromContext[pause.Manager](ctx)
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
log.SetStdLogger(boxInstance.LogFactory().Logger())
return i, nil
}

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
@@ -35,7 +36,6 @@ type StartedService struct {
handler PlatformHandler
debug bool
logMaxLines int
oomKiller bool
// workingDirectory string
// tempDirectory string
// userID int
@@ -56,9 +56,6 @@ type StartedService struct {
urlTestHistoryStorage *urltest.HistoryStorage
clashModeSubscriber *observable.Subscriber[struct{}]
clashModeObserver *observable.Observer[struct{}]
connectionEventSubscriber *observable.Subscriber[trafficontrol.ConnectionEvent]
connectionEventObserver *observable.Observer[trafficontrol.ConnectionEvent]
}
type ServiceOptions struct {
@@ -67,7 +64,6 @@ type ServiceOptions struct {
Handler PlatformHandler
Debug bool
LogMaxLines int
OOMKiller bool
// WorkingDirectory string
// TempDirectory string
// UserID int
@@ -82,25 +78,22 @@ func NewStartedService(options ServiceOptions) *StartedService {
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
oomKiller: options.OOMKiller,
// workingDirectory: options.WorkingDirectory,
// tempDirectory: options.TempDirectory,
// userID: options.UserID,
// groupID: options.GroupID,
// systemProxyEnabled: options.SystemProxyEnabled,
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
logSubscriber: observable.NewSubscriber[*log.Entry](128),
urlTestSubscriber: observable.NewSubscriber[struct{}](1),
urlTestHistoryStorage: urltest.NewHistoryStorage(),
clashModeSubscriber: observable.NewSubscriber[struct{}](1),
connectionEventSubscriber: observable.NewSubscriber[trafficontrol.ConnectionEvent](256),
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
logSubscriber: observable.NewSubscriber[*log.Entry](128),
urlTestSubscriber: observable.NewSubscriber[struct{}](1),
urlTestHistoryStorage: urltest.NewHistoryStorage(),
clashModeSubscriber: observable.NewSubscriber[struct{}](1),
}
s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
s.logObserver = observable.NewObserver(s.logSubscriber, 64)
s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
s.connectionEventObserver = observable.NewObserver(s.connectionEventSubscriber, 64)
return s
}
@@ -190,7 +183,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
instance.clashServer.(*clashapi.Server).TrafficManager().SetEventHook(s.connectionEventSubscriber)
}
s.serviceAccess.Unlock()
err = instance.Start()
@@ -209,14 +201,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
return nil
}
func (s *StartedService) Close() {
s.serviceStatusSubscriber.Close()
s.logSubscriber.Close()
s.urlTestSubscriber.Close()
s.clashModeSubscriber.Close()
s.connectionEventSubscriber.Close()
}
func (s *StartedService) CloseService() error {
s.serviceAccess.Lock()
switch s.serviceStatus.Status {
@@ -409,14 +393,12 @@ func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server
func (s *StartedService) readStatus() *Status {
var status Status
status.Memory = memory.Total()
status.Memory = memory.Inuse()
status.Goroutines = int32(runtime.NumGoroutine())
status.ConnectionsOut = int32(conntrack.Count())
s.serviceAccess.RLock()
nowService := s.instance
s.serviceAccess.RUnlock()
if nowService != nil && nowService.connectionManager != nil {
status.ConnectionsOut = int32(nowService.connectionManager.Count())
}
if nowService != nil {
if clashServer := nowService.clashServer; clashServer != nil {
status.TrafficAvailable = true
@@ -684,7 +666,7 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
return nil, err
}
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
@@ -692,260 +674,69 @@ func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsReque
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
if boxService.clashServer == nil {
return E.New("clash server not available")
}
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
subscription, done, err := s.connectionEventObserver.Subscribe()
if err != nil {
return err
}
defer s.connectionEventObserver.UnSubscribe(subscription)
connectionSnapshots := make(map[uuid.UUID]connectionSnapshot)
initialEvents := s.buildInitialConnectionState(trafficManager, connectionSnapshots)
err = server.Send(&ConnectionEvents{
Events: initialEvents,
Reset_: true,
})
if err != nil {
return err
}
interval := time.Duration(request.Interval)
if interval <= 0 {
interval = time.Second
}
ticker := time.NewTicker(interval)
ticker := time.NewTicker(time.Duration(request.Interval))
defer ticker.Stop()
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
var (
connections = make(map[uuid.UUID]*Connection)
outConnections []*Connection
)
for {
outConnections = outConnections[:0]
for _, connection := range trafficManager.Connections() {
outConnections = append(outConnections, newConnection(connections, connection, false))
}
for _, connection := range trafficManager.ClosedConnections() {
outConnections = append(outConnections, newConnection(connections, connection, true))
}
err := server.Send(&Connections{Connections: outConnections})
if err != nil {
return err
}
select {
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
case event := <-subscription:
var pendingEvents []*ConnectionEvent
if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil {
pendingEvents = append(pendingEvents, protoEvent)
}
drain:
for {
select {
case event = <-subscription:
if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil {
pendingEvents = append(pendingEvents, protoEvent)
}
default:
break drain
}
}
if len(pendingEvents) > 0 {
err = server.Send(&ConnectionEvents{Events: pendingEvents})
if err != nil {
return err
}
}
case <-ticker.C:
protoEvents := s.buildTrafficUpdates(trafficManager, connectionSnapshots)
if len(protoEvents) == 0 {
continue
}
err = server.Send(&ConnectionEvents{Events: protoEvents})
if err != nil {
return err
}
}
}
}
type connectionSnapshot struct {
uplink int64
downlink int64
hadTraffic bool
}
func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
var events []*ConnectionEvent
for _, metadata := range manager.Connections() {
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: metadata.ID.String(),
Connection: buildConnectionProto(metadata),
})
snapshots[metadata.ID] = connectionSnapshot{
uplink: metadata.Upload.Load(),
downlink: metadata.Download.Load(),
}
}
for _, metadata := range manager.ClosedConnections() {
conn := buildConnectionProto(metadata)
conn.ClosedAt = metadata.ClosedAt.UnixMilli()
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: metadata.ID.String(),
Connection: conn,
})
}
return events
}
func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent {
switch event.Type {
case trafficontrol.ConnectionEventNew:
if _, exists := snapshots[event.ID]; exists {
return nil
}
snapshots[event.ID] = connectionSnapshot{
uplink: event.Metadata.Upload.Load(),
downlink: event.Metadata.Download.Load(),
}
return &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: event.ID.String(),
Connection: buildConnectionProto(event.Metadata),
}
case trafficontrol.ConnectionEventClosed:
delete(snapshots, event.ID)
protoEvent := &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
Id: event.ID.String(),
}
closedAt := event.ClosedAt
if closedAt.IsZero() && !event.Metadata.ClosedAt.IsZero() {
closedAt = event.Metadata.ClosedAt
}
if closedAt.IsZero() {
closedAt = time.Now()
}
protoEvent.ClosedAt = closedAt.UnixMilli()
if event.Metadata.ID != uuid.Nil {
conn := buildConnectionProto(event.Metadata)
conn.ClosedAt = protoEvent.ClosedAt
protoEvent.Connection = conn
}
return protoEvent
default:
return nil
}
}
func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
activeConnections := manager.Connections()
activeIndex := make(map[uuid.UUID]*trafficontrol.TrackerMetadata, len(activeConnections))
var events []*ConnectionEvent
for _, metadata := range activeConnections {
activeIndex[metadata.ID] = metadata
currentUpload := metadata.Upload.Load()
currentDownload := metadata.Download.Load()
snapshot, exists := snapshots[metadata.ID]
if !exists {
snapshots[metadata.ID] = connectionSnapshot{
uplink: currentUpload,
downlink: currentDownload,
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) *Connection {
if oldConnection, loaded := connections[metadata.ID]; loaded {
if isClosed {
if oldConnection.ClosedAt == 0 {
oldConnection.Uplink = 0
oldConnection.Downlink = 0
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
}
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
Id: metadata.ID.String(),
Connection: buildConnectionProto(metadata),
})
continue
}
uplinkDelta := currentUpload - snapshot.uplink
downlinkDelta := currentDownload - snapshot.downlink
if uplinkDelta < 0 || downlinkDelta < 0 {
if snapshot.hadTraffic {
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
Id: metadata.ID.String(),
UplinkDelta: 0,
DownlinkDelta: 0,
})
}
snapshot.uplink = currentUpload
snapshot.downlink = currentDownload
snapshot.hadTraffic = false
snapshots[metadata.ID] = snapshot
continue
}
if uplinkDelta > 0 || downlinkDelta > 0 {
snapshot.uplink = currentUpload
snapshot.downlink = currentDownload
snapshot.hadTraffic = true
snapshots[metadata.ID] = snapshot
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
Id: metadata.ID.String(),
UplinkDelta: uplinkDelta,
DownlinkDelta: downlinkDelta,
})
continue
}
if snapshot.hadTraffic {
snapshot.uplink = currentUpload
snapshot.downlink = currentDownload
snapshot.hadTraffic = false
snapshots[metadata.ID] = snapshot
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
Id: metadata.ID.String(),
UplinkDelta: 0,
DownlinkDelta: 0,
})
return oldConnection
}
lastUplink := oldConnection.UplinkTotal
lastDownlink := oldConnection.DownlinkTotal
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
oldConnection.Uplink = uplinkTotal - lastUplink
oldConnection.Downlink = downlinkTotal - lastDownlink
oldConnection.UplinkTotal = uplinkTotal
oldConnection.DownlinkTotal = downlinkTotal
return oldConnection
}
var closedIndex map[uuid.UUID]*trafficontrol.TrackerMetadata
for id := range snapshots {
if _, exists := activeIndex[id]; exists {
continue
}
if closedIndex == nil {
closedIndex = make(map[uuid.UUID]*trafficontrol.TrackerMetadata)
for _, metadata := range manager.ClosedConnections() {
closedIndex[metadata.ID] = metadata
}
}
closedAt := time.Now()
var conn *Connection
if metadata, ok := closedIndex[id]; ok {
if !metadata.ClosedAt.IsZero() {
closedAt = metadata.ClosedAt
}
conn = buildConnectionProto(metadata)
conn.ClosedAt = closedAt.UnixMilli()
}
events = append(events, &ConnectionEvent{
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
Id: id.String(),
ClosedAt: closedAt.UnixMilli(),
Connection: conn,
})
delete(snapshots, id)
}
return events
}
func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
var rule string
if metadata.Rule != nil {
rule = metadata.Rule.String()
}
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
uplink := uplinkTotal
downlink := downlinkTotal
var closedAt int64
if !metadata.ClosedAt.IsZero() {
closedAt = metadata.ClosedAt.UnixMilli()
uplink = 0
downlink = 0
}
var processInfo *ProcessInfo
if metadata.Metadata.ProcessInfo != nil {
processInfo = &ProcessInfo{
@@ -956,7 +747,7 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
}
}
return &Connection{
connection := &Connection{
Id: metadata.ID.String(),
Inbound: metadata.Metadata.Inbound,
InboundType: metadata.Metadata.InboundType,
@@ -969,6 +760,9 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
User: metadata.Metadata.User,
FromOutbound: metadata.Metadata.Outbound,
CreatedAt: metadata.CreatedAt.UnixMilli(),
ClosedAt: closedAt,
Uplink: uplink,
Downlink: downlink,
UplinkTotal: uplinkTotal,
DownlinkTotal: downlinkTotal,
Rule: rule,
@@ -977,6 +771,8 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
ChainList: metadata.Chain,
ProcessInfo: processInfo,
}
connections[metadata.ID] = connection
return connection
}
func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) {
@@ -997,12 +793,7 @@ func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConn
}
func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
s.serviceAccess.RLock()
nowService := s.instance
s.serviceAccess.RUnlock()
if nowService != nil && nowService.connectionManager != nil {
nowService.connectionManager.CloseAll()
}
conntrack.Close()
return &emptypb.Empty{}, nil
}

View File

@@ -78,55 +78,104 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{0}
}
type ConnectionEventType int32
type ConnectionFilter int32
const (
ConnectionEventType_CONNECTION_EVENT_NEW ConnectionEventType = 0
ConnectionEventType_CONNECTION_EVENT_UPDATE ConnectionEventType = 1
ConnectionEventType_CONNECTION_EVENT_CLOSED ConnectionEventType = 2
ConnectionFilter_ALL ConnectionFilter = 0
ConnectionFilter_ACTIVE ConnectionFilter = 1
ConnectionFilter_CLOSED ConnectionFilter = 2
)
// Enum value maps for ConnectionEventType.
// Enum value maps for ConnectionFilter.
var (
ConnectionEventType_name = map[int32]string{
0: "CONNECTION_EVENT_NEW",
1: "CONNECTION_EVENT_UPDATE",
2: "CONNECTION_EVENT_CLOSED",
ConnectionFilter_name = map[int32]string{
0: "ALL",
1: "ACTIVE",
2: "CLOSED",
}
ConnectionEventType_value = map[string]int32{
"CONNECTION_EVENT_NEW": 0,
"CONNECTION_EVENT_UPDATE": 1,
"CONNECTION_EVENT_CLOSED": 2,
ConnectionFilter_value = map[string]int32{
"ALL": 0,
"ACTIVE": 1,
"CLOSED": 2,
}
)
func (x ConnectionEventType) Enum() *ConnectionEventType {
p := new(ConnectionEventType)
func (x ConnectionFilter) Enum() *ConnectionFilter {
p := new(ConnectionFilter)
*p = x
return p
}
func (x ConnectionEventType) String() string {
func (x ConnectionFilter) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ConnectionEventType) Descriptor() protoreflect.EnumDescriptor {
func (ConnectionFilter) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[1].Descriptor()
}
func (ConnectionEventType) Type() protoreflect.EnumType {
func (ConnectionFilter) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[1]
}
func (x ConnectionEventType) Number() protoreflect.EnumNumber {
func (x ConnectionFilter) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ConnectionEventType.Descriptor instead.
func (ConnectionEventType) EnumDescriptor() ([]byte, []int) {
// Deprecated: Use ConnectionFilter.Descriptor instead.
func (ConnectionFilter) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{1}
}
type ConnectionSortBy int32
const (
ConnectionSortBy_DATE ConnectionSortBy = 0
ConnectionSortBy_TRAFFIC ConnectionSortBy = 1
ConnectionSortBy_TOTAL_TRAFFIC ConnectionSortBy = 2
)
// Enum value maps for ConnectionSortBy.
var (
ConnectionSortBy_name = map[int32]string{
0: "DATE",
1: "TRAFFIC",
2: "TOTAL_TRAFFIC",
}
ConnectionSortBy_value = map[string]int32{
"DATE": 0,
"TRAFFIC": 1,
"TOTAL_TRAFFIC": 2,
}
)
func (x ConnectionSortBy) Enum() *ConnectionSortBy {
p := new(ConnectionSortBy)
*p = x
return p
}
func (x ConnectionSortBy) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ConnectionSortBy) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[2].Descriptor()
}
func (ConnectionSortBy) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[2]
}
func (x ConnectionSortBy) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ConnectionSortBy.Descriptor instead.
func (ConnectionSortBy) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{2}
}
type ServiceStatus_Type int32
const (
@@ -166,11 +215,11 @@ func (x ServiceStatus_Type) String() string {
}
func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[2].Descriptor()
return file_daemon_started_service_proto_enumTypes[3].Descriptor()
}
func (ServiceStatus_Type) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[2]
return &file_daemon_started_service_proto_enumTypes[3]
}
func (x ServiceStatus_Type) Number() protoreflect.EnumNumber {
@@ -1065,6 +1114,8 @@ func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
type SubscribeConnectionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
Filter ConnectionFilter `protobuf:"varint,2,opt,name=filter,proto3,enum=daemon.ConnectionFilter" json:"filter,omitempty"`
SortBy ConnectionSortBy `protobuf:"varint,3,opt,name=sortBy,proto3,enum=daemon.ConnectionSortBy" json:"sortBy,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1106,32 +1157,41 @@ func (x *SubscribeConnectionsRequest) GetInterval() int64 {
return 0
}
type ConnectionEvent struct {
func (x *SubscribeConnectionsRequest) GetFilter() ConnectionFilter {
if x != nil {
return x.Filter
}
return ConnectionFilter_ALL
}
func (x *SubscribeConnectionsRequest) GetSortBy() ConnectionSortBy {
if x != nil {
return x.SortBy
}
return ConnectionSortBy_DATE
}
type Connections struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type ConnectionEventType `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.ConnectionEventType" json:"type,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
Connection *Connection `protobuf:"bytes,3,opt,name=connection,proto3" json:"connection,omitempty"`
UplinkDelta int64 `protobuf:"varint,4,opt,name=uplinkDelta,proto3" json:"uplinkDelta,omitempty"`
DownlinkDelta int64 `protobuf:"varint,5,opt,name=downlinkDelta,proto3" json:"downlinkDelta,omitempty"`
ClosedAt int64 `protobuf:"varint,6,opt,name=closedAt,proto3" json:"closedAt,omitempty"`
Connections []*Connection `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnectionEvent) Reset() {
*x = ConnectionEvent{}
func (x *Connections) Reset() {
*x = Connections{}
mi := &file_daemon_started_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnectionEvent) String() string {
func (x *Connections) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnectionEvent) ProtoMessage() {}
func (*Connections) ProtoMessage() {}
func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
func (x *Connections) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1143,105 +1203,18 @@ func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead.
func (*ConnectionEvent) Descriptor() ([]byte, []int) {
// Deprecated: Use Connections.ProtoReflect.Descriptor instead.
func (*Connections) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
}
func (x *ConnectionEvent) GetType() ConnectionEventType {
func (x *Connections) GetConnections() []*Connection {
if x != nil {
return x.Type
}
return ConnectionEventType_CONNECTION_EVENT_NEW
}
func (x *ConnectionEvent) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ConnectionEvent) GetConnection() *Connection {
if x != nil {
return x.Connection
return x.Connections
}
return nil
}
func (x *ConnectionEvent) GetUplinkDelta() int64 {
if x != nil {
return x.UplinkDelta
}
return 0
}
func (x *ConnectionEvent) GetDownlinkDelta() int64 {
if x != nil {
return x.DownlinkDelta
}
return 0
}
func (x *ConnectionEvent) GetClosedAt() int64 {
if x != nil {
return x.ClosedAt
}
return 0
}
type ConnectionEvents struct {
state protoimpl.MessageState `protogen:"open.v1"`
Events []*ConnectionEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnectionEvents) Reset() {
*x = ConnectionEvents{}
mi := &file_daemon_started_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnectionEvents) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnectionEvents) ProtoMessage() {}
func (x *ConnectionEvents) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead.
func (*ConnectionEvents) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
}
func (x *ConnectionEvents) GetEvents() []*ConnectionEvent {
if x != nil {
return x.Events
}
return nil
}
func (x *ConnectionEvents) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type Connection struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -1272,7 +1245,7 @@ type Connection struct {
func (x *Connection) Reset() {
*x = Connection{}
mi := &file_daemon_started_service_proto_msgTypes[19]
mi := &file_daemon_started_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1284,7 +1257,7 @@ func (x *Connection) String() string {
func (*Connection) ProtoMessage() {}
func (x *Connection) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[19]
mi := &file_daemon_started_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1297,7 +1270,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message {
// Deprecated: Use Connection.ProtoReflect.Descriptor instead.
func (*Connection) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
}
func (x *Connection) GetId() string {
@@ -1467,7 +1440,7 @@ type ProcessInfo struct {
func (x *ProcessInfo) Reset() {
*x = ProcessInfo{}
mi := &file_daemon_started_service_proto_msgTypes[20]
mi := &file_daemon_started_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1479,7 +1452,7 @@ func (x *ProcessInfo) String() string {
func (*ProcessInfo) ProtoMessage() {}
func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[20]
mi := &file_daemon_started_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1492,7 +1465,7 @@ func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead.
func (*ProcessInfo) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
}
func (x *ProcessInfo) GetProcessId() uint32 {
@@ -1539,7 +1512,7 @@ type CloseConnectionRequest struct {
func (x *CloseConnectionRequest) Reset() {
*x = CloseConnectionRequest{}
mi := &file_daemon_started_service_proto_msgTypes[21]
mi := &file_daemon_started_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1551,7 +1524,7 @@ func (x *CloseConnectionRequest) String() string {
func (*CloseConnectionRequest) ProtoMessage() {}
func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[21]
mi := &file_daemon_started_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1564,7 +1537,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead.
func (*CloseConnectionRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
}
func (x *CloseConnectionRequest) GetId() string {
@@ -1583,7 +1556,7 @@ type DeprecatedWarnings struct {
func (x *DeprecatedWarnings) Reset() {
*x = DeprecatedWarnings{}
mi := &file_daemon_started_service_proto_msgTypes[22]
mi := &file_daemon_started_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1595,7 +1568,7 @@ func (x *DeprecatedWarnings) String() string {
func (*DeprecatedWarnings) ProtoMessage() {}
func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[22]
mi := &file_daemon_started_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1608,7 +1581,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead.
func (*DeprecatedWarnings) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
}
func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
@@ -1629,7 +1602,7 @@ type DeprecatedWarning struct {
func (x *DeprecatedWarning) Reset() {
*x = DeprecatedWarning{}
mi := &file_daemon_started_service_proto_msgTypes[23]
mi := &file_daemon_started_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1641,7 +1614,7 @@ func (x *DeprecatedWarning) String() string {
func (*DeprecatedWarning) ProtoMessage() {}
func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[23]
mi := &file_daemon_started_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1654,7 +1627,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead.
func (*DeprecatedWarning) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
}
func (x *DeprecatedWarning) GetMessage() string {
@@ -1687,7 +1660,7 @@ type StartedAt struct {
func (x *StartedAt) Reset() {
*x = StartedAt{}
mi := &file_daemon_started_service_proto_msgTypes[24]
mi := &file_daemon_started_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1699,7 +1672,7 @@ func (x *StartedAt) String() string {
func (*StartedAt) ProtoMessage() {}
func (x *StartedAt) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[24]
mi := &file_daemon_started_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1712,7 +1685,7 @@ func (x *StartedAt) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartedAt.ProtoReflect.Descriptor instead.
func (*StartedAt) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{24}
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
}
func (x *StartedAt) GetStartedAt() int64 {
@@ -1732,7 +1705,7 @@ type Log_Message struct {
func (x *Log_Message) Reset() {
*x = Log_Message{}
mi := &file_daemon_started_service_proto_msgTypes[25]
mi := &file_daemon_started_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1744,7 +1717,7 @@ func (x *Log_Message) String() string {
func (*Log_Message) ProtoMessage() {}
func (x *Log_Message) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[25]
mi := &file_daemon_started_service_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1845,21 +1818,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
"\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"9\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"\x9d\x01\n" +
"\x1bSubscribeConnectionsRequest\x12\x1a\n" +
"\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" +
"\x0fConnectionEvent\x12/\n" +
"\x04type\x18\x01 \x01(\x0e2\x1b.daemon.ConnectionEventTypeR\x04type\x12\x0e\n" +
"\x02id\x18\x02 \x01(\tR\x02id\x122\n" +
"\n" +
"connection\x18\x03 \x01(\v2\x12.daemon.ConnectionR\n" +
"connection\x12 \n" +
"\vuplinkDelta\x18\x04 \x01(\x03R\vuplinkDelta\x12$\n" +
"\rdownlinkDelta\x18\x05 \x01(\x03R\rdownlinkDelta\x12\x1a\n" +
"\bclosedAt\x18\x06 \x01(\x03R\bclosedAt\"Y\n" +
"\x10ConnectionEvents\x12/\n" +
"\x06events\x18\x01 \x03(\v2\x17.daemon.ConnectionEventR\x06events\x12\x14\n" +
"\x05reset\x18\x02 \x01(\bR\x05reset\"\x95\x05\n" +
"\binterval\x18\x01 \x01(\x03R\binterval\x120\n" +
"\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" +
"\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" +
"\vConnections\x124\n" +
"\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\x95\x05\n" +
"\n" +
"Connection\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
@@ -1908,11 +1873,17 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x04WARN\x10\x03\x12\b\n" +
"\x04INFO\x10\x04\x12\t\n" +
"\x05DEBUG\x10\x05\x12\t\n" +
"\x05TRACE\x10\x06*i\n" +
"\x13ConnectionEventType\x12\x18\n" +
"\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" +
"\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" +
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xe5\v\n" +
"\x05TRACE\x10\x06*3\n" +
"\x10ConnectionFilter\x12\a\n" +
"\x03ALL\x10\x00\x12\n" +
"\n" +
"\x06ACTIVE\x10\x01\x12\n" +
"\n" +
"\x06CLOSED\x10\x02*<\n" +
"\x10ConnectionSortBy\x12\b\n" +
"\x04DATE\x10\x00\x12\v\n" +
"\aTRAFFIC\x10\x01\x12\x11\n" +
"\rTOTAL_TRAFFIC\x10\x022\xe0\v\n" +
"\x0eStartedService\x12=\n" +
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
@@ -1929,8 +1900,8 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
"\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" +
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" +
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\x000\x01\x12K\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n" +
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x13.daemon.Connections\"\x000\x01\x12K\n" +
"\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
"\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" +
"\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" +
@@ -1949,31 +1920,31 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
}
var (
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
file_daemon_started_service_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType
(ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type
(*ServiceStatus)(nil), // 3: daemon.ServiceStatus
(*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest
(*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest
(*Log)(nil), // 6: daemon.Log
(*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel
(*Status)(nil), // 8: daemon.Status
(*Groups)(nil), // 9: daemon.Groups
(*Group)(nil), // 10: daemon.Group
(*GroupItem)(nil), // 11: daemon.GroupItem
(*URLTestRequest)(nil), // 12: daemon.URLTestRequest
(*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest
(*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest
(*ClashMode)(nil), // 15: daemon.ClashMode
(*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus
(*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus
(*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest
(*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest
(*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent
(*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents
(ConnectionFilter)(0), // 1: daemon.ConnectionFilter
(ConnectionSortBy)(0), // 2: daemon.ConnectionSortBy
(ServiceStatus_Type)(0), // 3: daemon.ServiceStatus.Type
(*ServiceStatus)(nil), // 4: daemon.ServiceStatus
(*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest
(*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest
(*Log)(nil), // 7: daemon.Log
(*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel
(*Status)(nil), // 9: daemon.Status
(*Groups)(nil), // 10: daemon.Groups
(*Group)(nil), // 11: daemon.Group
(*GroupItem)(nil), // 12: daemon.GroupItem
(*URLTestRequest)(nil), // 13: daemon.URLTestRequest
(*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest
(*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest
(*ClashMode)(nil), // 16: daemon.ClashMode
(*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus
(*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus
(*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest
(*SubscribeConnectionsRequest)(nil), // 20: daemon.SubscribeConnectionsRequest
(*Connections)(nil), // 21: daemon.Connections
(*Connection)(nil), // 22: daemon.Connection
(*ProcessInfo)(nil), // 23: daemon.ProcessInfo
(*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest
@@ -1986,14 +1957,14 @@ var (
)
var file_daemon_started_service_proto_depIdxs = []int32{
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
3, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
10, // 3: daemon.Groups.group:type_name -> daemon.Group
11, // 4: daemon.Group.items:type_name -> daemon.GroupItem
1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType
22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection
20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent
11, // 3: daemon.Groups.group:type_name -> daemon.Group
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
1, // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter
2, // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy
22, // 7: daemon.Connections.connections:type_name -> daemon.Connection
23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel
@@ -2003,38 +1974,38 @@ var file_daemon_started_service_proto_depIdxs = []int32{
29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
6, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
16, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
13, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
14, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
15, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
19, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
20, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
4, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
9, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
18, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
@@ -2056,8 +2027,8 @@ func file_daemon_started_service_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
NumEnums: 3,
NumMessages: 26,
NumEnums: 4,
NumMessages: 25,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -27,7 +27,7 @@ service StartedService {
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream Connections) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
@@ -143,26 +143,24 @@ message SetSystemProxyEnabledRequest {
message SubscribeConnectionsRequest {
int64 interval = 1;
ConnectionFilter filter = 2;
ConnectionSortBy sortBy = 3;
}
enum ConnectionEventType {
CONNECTION_EVENT_NEW = 0;
CONNECTION_EVENT_UPDATE = 1;
CONNECTION_EVENT_CLOSED = 2;
enum ConnectionFilter {
ALL = 0;
ACTIVE = 1;
CLOSED = 2;
}
message ConnectionEvent {
ConnectionEventType type = 1;
string id = 2;
Connection connection = 3;
int64 uplinkDelta = 4;
int64 downlinkDelta = 5;
int64 closedAt = 6;
enum ConnectionSortBy {
DATE = 0;
TRAFFIC = 1;
TOTAL_TRAFFIC = 2;
}
message ConnectionEvents {
repeated ConnectionEvent events = 1;
bool reset = 2;
message Connections {
repeated Connection connections = 1;
}
message Connection {

View File

@@ -58,7 +58,7 @@ type StartedServiceClient interface {
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error)
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
@@ -278,13 +278,13 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
return out, nil
}
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{ClientStream: stream}
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, Connections]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
@@ -295,7 +295,7 @@ func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *Sub
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[ConnectionEvents]
type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[Connections]
func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
@@ -357,7 +357,7 @@ type StartedServiceServer interface {
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
@@ -373,87 +373,87 @@ type StartedServiceServer interface {
type UnimplementedStartedServiceServer struct{}
func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
return nil, status.Errorf(codes.Unimplemented, "method StopService not implemented")
}
func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ReloadService not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
return status.Errorf(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
return status.Error(codes.Unimplemented, "method SubscribeLog not implemented")
return status.Errorf(codes.Unimplemented, "method SubscribeLog not implemented")
}
func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
}
func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ClearLogs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ClearLogs not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented")
return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented")
return status.Errorf(codes.Unimplemented, "method SubscribeGroups not implemented")
}
func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetClashModeStatus not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented")
return status.Errorf(codes.Unimplemented, "method SubscribeClashMode not implemented")
}
func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetClashMode not implemented")
}
func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method URLTest not implemented")
return nil, status.Errorf(codes.Unimplemented, "method URLTest not implemented")
}
func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SelectOutbound not implemented")
}
func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetGroupExpand not implemented")
}
func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
}
func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeConnections not implemented")
}
func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CloseConnection not implemented")
}
func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CloseAllConnections not implemented")
}
func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
}
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -466,7 +466,7 @@ type UnsafeStartedServiceServer interface {
}
func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) {
// If the following call panics, it indicates UnimplementedStartedServiceServer was
// If the following call pancis, it indicates UnimplementedStartedServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@@ -734,11 +734,11 @@ func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.S
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{ServerStream: stream})
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, Connections]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[ConnectionEvents]
type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[Connections]
func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CloseConnectionRequest)

View File

@@ -3,11 +3,11 @@ package box
import (
"runtime/debug"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func applyDebugOptions(options option.DebugOptions) error {
func applyDebugOptions(options option.DebugOptions) {
applyDebugListenOption(options)
if options.GCPercent != nil {
debug.SetGCPercent(*options.GCPercent)
@@ -26,9 +26,9 @@ func applyDebugOptions(options option.DebugOptions) error {
}
if options.MemoryLimit.Value() != 0 {
debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5))
conntrack.MemoryLimit = options.MemoryLimit.Value()
}
if options.OOMKiller != nil {
return E.New("legacy oom_killer in debug options is removed, use oom-killer service instead")
conntrack.KillerEnabled = *options.OOMKiller
}
return nil
}

View File

@@ -144,11 +144,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
<-cond
} else {
defer func() {
c.cacheLock.Delete(question)
@@ -158,11 +154,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
<-cond
} else {
defer func() {
c.transportCacheLock.Delete(question)
@@ -240,10 +232,8 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
if response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0 {
rejected = true
} else if len(response.Answer) == 0 {
rejected = !responseChecker(nil)
} else {
rejected = !responseChecker(MessageToAddresses(response))
}
@@ -324,20 +314,16 @@ 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, lookupOptions, responseChecker)
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, 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, lookupOptions, responseChecker)
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
if err != nil {
return err
}
@@ -345,7 +331,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, lookupOptions, responseChecker)
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
if err != nil {
return err
}

View File

@@ -195,16 +195,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
}
}
}
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
return r.transport.Default(), nil, -1
}
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
@@ -281,7 +272,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
return action.Response(message), nil
}
}
responseCheck := addressLimitResponseCheck(rule, metadata)
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
@@ -354,7 +351,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 = legacyTransport.LegacyStrategy()
options.Strategy = r.defaultDomainStrategy
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
@@ -380,11 +377,9 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
case *R.RuleActionReject:
return nil, &R.RejectedError{Cause: action.Error(ctx)}
case *R.RuleActionPredefined:
responseAddrs = nil
if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode)
} else {
err = nil
for _, answer := range action.Answer {
switch record := answer.(type) {
case *mDNS.A:
@@ -397,7 +392,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
goto response
}
}
responseCheck := addressLimitResponseCheck(rule, metadata)
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
@@ -425,18 +426,6 @@ func isAddressQuery(message *mDNS.Msg) bool {
return false
}
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
if rule == nil || !rule.WithAddressLimit() {
return nil
}
responseMetadata := *metadata
return func(responseAddrs []netip.Addr) bool {
checkMetadata := responseMetadata
checkMetadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(&checkMetadata)
}
}
func (r *Router) ClearCache() {
r.client.ClearCache()
if r.platformInterface != nil {

View File

@@ -4,9 +4,6 @@ import (
"context"
"net"
"sync"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
type ConnectorCallbacks[T any] struct {
@@ -19,11 +16,10 @@ type Connector[T any] struct {
dial func(ctx context.Context) (T, error)
callbacks ConnectorCallbacks[T]
access sync.Mutex
connection T
hasConnection bool
connectionCancel context.CancelFunc
connecting chan struct{}
access sync.Mutex
connection T
hasConnection bool
connecting chan struct{}
closeCtx context.Context
closed bool
@@ -51,10 +47,6 @@ func NewSingleflightConnector(closeCtx context.Context, dial func(context.Contex
})
}
type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
@@ -72,14 +64,6 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
}
c.hasConnection = false
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if isRecursiveConnectorDial(ctx, c) {
c.access.Unlock()
return zero, errRecursiveConnectorDial
}
if c.connecting != nil {
connecting := c.connecting
@@ -95,16 +79,10 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
}
}
if err := ctx.Err(); err != nil {
c.access.Unlock()
return zero, err
}
c.connecting = make(chan struct{})
c.access.Unlock()
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
connection, cancel, err := c.dialWithCancellation(dialContext)
connection, err := c.dialWithCancellation(ctx)
c.access.Lock()
close(c.connecting)
@@ -116,21 +94,13 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
}
if c.closed {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
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()
@@ -138,63 +108,19 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
}
}
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
return loaded && dialConnector == connector
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, error) {
dialCtx, cancel := context.WithCancel(ctx)
defer cancel()
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, nil, err
}
connCtx, cancel := context.WithCancel(c.closeCtx)
var (
stateAccess sync.Mutex
dialComplete bool
)
stopCancel := context.AfterFunc(ctx, func() {
stateAccess.Lock()
if !dialComplete {
go func() {
select {
case <-c.closeCtx.Done():
cancel()
case <-dialCtx.Done():
}
stateAccess.Unlock()
})
select {
case <-ctx.Done():
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
cancel()
return zero, nil, ctx.Err()
default:
}
}()
connection, err := c.dial(valueContext{connCtx, ctx})
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
if err != nil {
cancel()
return zero, nil, err
}
return connection, cancel, nil
}
type valueContext struct {
context.Context
parent context.Context
}
func (v valueContext) Value(key any) any {
return v.parent.Value(key)
}
func (v valueContext) Deadline() (time.Time, bool) {
return v.parent.Deadline()
return c.dial(dialCtx)
}
func (c *Connector[T]) Close() error {
@@ -206,10 +132,6 @@ func (c *Connector[T]) Close() error {
}
c.closed = true
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if c.hasConnection {
c.callbacks.Close(c.connection)
c.hasConnection = false
@@ -222,10 +144,6 @@ func (c *Connector[T]) Reset() {
c.access.Lock()
defer c.access.Unlock()
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if c.hasConnection {
c.callbacks.Reset(c.connection)
c.hasConnection = false

View File

@@ -1,263 +0,0 @@
package transport
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type testConnectorConnection struct{}
func TestConnectorRecursiveGetFailsFast(t *testing.T) {
t.Parallel()
var (
dialCount atomic.Int32
closeCount atomic.Int32
connector *Connector[*testConnectorConnection]
)
dial := func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
_, err := connector.Get(ctx)
if err != nil {
return nil, err
}
return &testConnectorConnection{}, nil
}
connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
Reset: func(connection *testConnectorConnection) {
closeCount.Add(1)
},
})
_, err := connector.Get(context.Background())
require.ErrorIs(t, err, errRecursiveConnectorDial)
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 0, closeCount.Load())
}
func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) {
t.Parallel()
var (
outerDialCount atomic.Int32
innerDialCount atomic.Int32
outerConnector *Connector[*testConnectorConnection]
innerConnector *Connector[*testConnectorConnection]
)
innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
innerDialCount.Add(1)
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
outerDialCount.Add(1)
_, err := innerConnector.Get(ctx)
if err != nil {
return nil, err
}
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
_, err := outerConnector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 1, outerDialCount.Load())
require.EqualValues(t, 1, innerDialCount.Load())
}
func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) {
t.Parallel()
type contextKey struct{}
var (
dialValue any
dialDeadline time.Time
dialHasDeadline bool
)
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialValue = ctx.Value(contextKey{})
dialDeadline, dialHasDeadline = ctx.Deadline()
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
deadline := time.Now().Add(time.Minute)
requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline)
defer cancel()
_, err := connector.Get(requestContext)
require.NoError(t, err)
require.Equal(t, "test-value", dialValue)
require.True(t, dialHasDeadline)
require.WithinDuration(t, deadline, dialDeadline, time.Second)
}
func TestConnectorDialSkipsCanceledRequest(t *testing.T) {
t.Parallel()
var dialCount atomic.Int32
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialCount.Add(1)
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
cancel()
_, err := connector.Get(requestContext)
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 0, dialCount.Load())
}
func TestConnectorCanceledRequestDoesNotCacheConnection(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()
close(releaseDial)
err := <-result
require.ErrorIs(t, err, context.Canceled)
require.EqualValues(t, 1, dialCount.Load())
require.EqualValues(t, 1, closeCount.Load())
_, err = connector.Get(context.Background())
require.NoError(t, err)
require.EqualValues(t, 2, dialCount.Load())
}
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
t.Parallel()
var dialContext context.Context
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialContext = ctx
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
requestContext, cancel := context.WithCancel(context.Background())
_, err := connector.Get(requestContext)
require.NoError(t, err)
require.NotNil(t, dialContext)
cancel()
select {
case <-dialContext.Done():
t.Fatal("dial context canceled by request context after successful dial")
case <-time.After(100 * time.Millisecond):
}
err = connector.Close()
require.NoError(t, err)
}
func TestConnectorDialContextCanceledOnClose(t *testing.T) {
t.Parallel()
var dialContext context.Context
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
dialContext = ctx
return &testConnectorConnection{}, nil
}, ConnectorCallbacks[*testConnectorConnection]{
IsClosed: func(connection *testConnectorConnection) bool {
return false
},
Close: func(connection *testConnectorConnection) {},
Reset: func(connection *testConnectorConnection) {},
})
_, err := connector.Get(context.Background())
require.NoError(t, err)
require.NotNil(t, dialContext)
select {
case <-dialContext.Done():
t.Fatal("dial context canceled before connector close")
default:
}
err = connector.Close()
require.NoError(t, err)
select {
case <-dialContext.Done():
case <-time.After(time.Second):
t.Fatal("dial context not canceled after connector close")
}
}

View File

@@ -18,8 +18,6 @@ type Store struct {
logger logger.Logger
inet4Range netip.Prefix
inet6Range netip.Prefix
inet4Last netip.Addr
inet6Last netip.Addr
storage adapter.FakeIPStorage
addressAccess sync.Mutex
@@ -28,35 +26,12 @@ type Store struct {
}
func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
store := &Store{
return &Store{
ctx: ctx,
logger: logger,
inet4Range: inet4Range,
inet6Range: inet6Range,
}
if inet4Range.IsValid() {
store.inet4Last = broadcastAddress(inet4Range)
}
if inet6Range.IsValid() {
store.inet6Last = broadcastAddress(inet6Range)
}
return store
}
func broadcastAddress(prefix netip.Prefix) netip.Addr {
addr := prefix.Addr()
raw := addr.As16()
bits := prefix.Bits()
if addr.Is4() {
bits += 96
}
for i := bits; i < 128; i++ {
raw[i/8] |= 1 << (7 - i%8)
}
if addr.Is4() {
return netip.AddrFrom4([4]byte(raw[12:]))
}
return netip.AddrFrom16(raw)
}
func (s *Store) Start() error {
@@ -74,10 +49,10 @@ func (s *Store) Start() error {
s.inet6Current = metadata.Inet6Current
} else {
if s.inet4Range.IsValid() {
s.inet4Current = s.inet4Range.Addr().Next()
s.inet4Current = s.inet4Range.Addr().Next().Next()
}
if s.inet6Range.IsValid() {
s.inet6Current = s.inet6Range.Addr().Next()
s.inet6Current = s.inet6Range.Addr().Next().Next()
}
_ = storage.FakeIPReset()
}
@@ -123,7 +98,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
return netip.Addr{}, E.New("missing IPv4 fakeip address range")
}
nextAddress := s.inet4Current.Next()
if nextAddress == s.inet4Last || !s.inet4Range.Contains(nextAddress) {
if !s.inet4Range.Contains(nextAddress) {
nextAddress = s.inet4Range.Addr().Next().Next()
}
s.inet4Current = nextAddress
@@ -133,7 +108,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
return netip.Addr{}, E.New("missing IPv6 fakeip address range")
}
nextAddress := s.inet6Current.Next()
if nextAddress == s.inet6Last || !s.inet6Range.Contains(nextAddress) {
if !s.inet6Range.Contains(nextAddress) {
nextAddress = s.inet6Range.Addr().Next().Next()
}
s.inet6Current = nextAddress

View File

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

View File

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

View File

@@ -4,26 +4,19 @@ 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"
@@ -56,23 +49,13 @@ type DBusResolvedResolver struct {
interfaceMonitor tun.DefaultInterfaceMonitor
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
systemBus *dbus.Conn
savedServerSet atomic.Pointer[resolvedServerSet]
resoledObject atomic.Pointer[ResolvedObject]
closeOnce sync.Once
}
type resolvedServerSet struct {
servers []resolvedServer
}
type resolvedServer struct {
primaryTransport adapter.DNSTransport
fallbackTransport adapter.DNSTransport
}
type resolvedServerSpecification struct {
address netip.Addr
port uint16
serverName string
type ResolvedObject struct {
dbus.BusObject
InterfaceIndex int32
}
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
@@ -99,31 +82,17 @@ func (t *DBusResolvedResolver) Start() error {
"org.freedesktop.DBus",
"NameOwnerChanged",
dbus.WithMatchSender("org.freedesktop.DBus"),
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")
return E.Cause(err, "configure resolved restart 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)
}
@@ -131,97 +100,99 @@ func (t *DBusResolvedResolver) Close() error {
_ = t.systemBus.Close()
}
})
return closeErr
return nil
}
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()
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()
}
return nil, E.Cause(call.Err, " resolve record via resolved")
}
response, err := t.exchangeServerSet(ctx, message, serverSet)
if err == nil {
return response, nil
}
t.updateStatus()
refreshedServerSet := t.savedServerSet.Load()
if refreshedServerSet == nil || refreshedServerSet == serverSet {
var (
records []resolved.ResourceRecord
outflags uint64
)
err := call.Store(&records, &outflags)
if err != nil {
return nil, err
}
return t.exchangeServerSet(ctx, message, refreshedServerSet)
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
}
func (t *DBusResolvedResolver) loopUpdateStatus() {
signalChan := make(chan *dbus.Signal, 1)
t.systemBus.Signal(signalChan)
for signal := range signalChan {
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) {
var restarted bool
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
continue
} else {
restarted = true
}
}
if restarted {
t.updateStatus()
}
}
}
func (t *DBusResolvedResolver) updateStatus() {
serverSet, err := t.checkResolved(context.Background())
oldServerSet := t.savedServerSet.Swap(serverSet)
if oldServerSet != nil {
_ = oldServerSet.Close()
}
dbusObject, err := t.checkResolved(context.Background())
oldValue := t.resoledObject.Swap(dbusObject)
if err != nil {
var dbusErr dbus.Error
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" {
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" {
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
}
if oldServerSet != nil {
if oldValue != nil {
t.logger.Debug("systemd-resolved service is gone")
}
return
} else if oldServerSet == nil {
} else if oldValue == nil {
t.logger.Debug("using systemd-resolved service as resolver")
}
}
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) {
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) {
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
if err != nil {
@@ -249,19 +220,16 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
if linkObject == nil {
return nil, E.New("missing link object for default interface")
}
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
if err != nil {
return nil, err
}
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
var linkDNS []resolved.LinkDNS
err = dnsProp.Store(&linkDNS)
if err != nil {
return nil, err
}
linkDNS, err := loadResolvedLinkDNS(linkObject)
if err != nil {
return nil, err
}
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
if 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")
@@ -269,233 +237,12 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
}
return nil, E.New("link has no DNS servers configured")
}
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,
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, 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

@@ -5,7 +5,6 @@ import (
"net"
"net/netip"
"os"
"strconv"
"syscall"
"time"
"unsafe"
@@ -64,9 +63,6 @@ func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
continue
}
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
if sockaddr.ZoneId != 0 {
dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10))
}
default:
// Unexpected type.
continue

View File

@@ -2,358 +2,6 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.2
* 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.3-beta.1
* 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.14.0-alpha.1
* Add `source_mac_address` and `source_hostname` rule items **1**
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
* Update NaiveProxy to 145.0.7632.159 **3**
* Fixes and improvements
**1**:
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
Supported on Linux, macOS, or in graphical clients on Android and macOS.
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
**2**:
Limit or exclude devices from TUN routing by MAC address.
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
See [TUN](/configuration/inbound/tun/#include_mac_address).
**3**:
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
#### 1.13.2
* Fixes and improvements
#### 1.13.1
* Fixes and improvements
#### 1.12.14
* Backport fixes
#### 1.13.0
Important changes since 1.12:
* Add NaiveProxy outbound **1**
* Add pre-match support for `auto_redirect` **2**
* Improve `auto_redirect` **3**
* Add Chrome Root Store certificate option **4**
* Add new options for ACME DNS-01 challenge providers **5**
* Add Wi-Fi state support for Linux and Windows **6**
* Add curve preferences, pinned public key SHA256, mTLS and ECH `query_server_name` for TLS options **7**
* Add kTLS support **8**
* Add ICMP echo (ping) proxy support **9**
* Add `interface_address`, `network_interface_address` and `default_interface_address` rule items **10**
* Add `preferred_by` route rule item **11**
* Improve `local` DNS server **12**
* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for listen and dial fields **13**
* Add `bind_address_no_port` option for dial fields **14**
* Add system interface, relay server and advertise tags options for Tailscale endpoint **15**
* Add Claude Code Multiplexer service **16**
* Add OpenAI Codex Multiplexer service **17**
* Apple/Android: Refactor GUI
* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs)
* Android: Add support for resisting VPN detection via Xposed
* Drop support for go1.23 **18**
* Drop support for Android 5.0 **19**
* Update uTLS to v1.8.2 **20**
* Update quic-go to v0.59.0
* Update gVisor to v20250811
* Update Tailscale to v1.92.4
**1**:
NaiveProxy outbound now supports QUIC, ECH, UDP over TCP, and configurable QUIC congestion control.
Only available on Apple platforms, Android, Windows and some Linux architectures.
Each Windows release includes `libcronet.dll`
ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`.
See [NaiveProxy outbound](/configuration/outbound/naive/).
**2**:
`auto_redirect` now allows you to bypass sing-box for connections based on routing rules.
A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly.
This feature requires Linux with `auto_redirect` enabled.
See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass).
**3**:
`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues.
You can change it to bypass sing-box via the new `exclude_mptcp` option.
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
ensuring traffic is routed to the sing-box table when no route is found in system tables.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
See [TUN](/configuration/inbound/tun/#exclude_mptcp).
**4**:
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
See [Certificate](/configuration/certificate/#store).
**5**:
See [DNS-01 Challenge](/configuration/shared/dns01_challenge/).
**6**:
sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`.
See [Wi-Fi State](/configuration/shared/wifi-state/).
**7**:
See [TLS](/configuration/shared/tls/).
**8**:
Adds `kernel_tx` and `kernel_rx` options for TLS inbound.
Enables kernel-level TLS offloading via `splice(2)` on Linux 5.1+ with TLS 1.3.
See [TLS](/configuration/shared/tls/).
**9**:
sing-box can now proxy ICMP echo (ping) requests.
A new `icmp` network type is available for route rules.
Supported from TUN, WireGuard and Tailscale inbounds to Direct, WireGuard and Tailscale outbounds.
The `reject` action can also reply to ICMP echo requests.
**10**:
New rule items for matching based on interface IP addresses, available in route rules, DNS rules and rule-sets.
**11**:
Matches outbounds' preferred routes.
For Tailscale: MagicDNS domains and peers' allowed IPs. For WireGuard: peers' allowed IPs.
**12**:
The `local` DNS server now uses platform-native resolution:
`getaddrinfo`/libresolv on Apple platforms, systemd-resolved DBus on Linux.
A new `prefer_go` option is available to opt out.
See [Local DNS](/configuration/dns/server/local/).
**13**:
The default TCP keep-alive initial period has been updated from 10 minutes to 5 minutes.
See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive).
**14**:
Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address.
This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios.
See [Dial Fields](/configuration/shared/dial/#bind_address_no_port).
**15**:
Tailscale endpoint can now create a system TUN interface to handle traffic directly.
New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections.
New `advertise_tags` option for ACL tag advertisement.
See [Tailscale endpoint](/configuration/endpoint/tailscale/).
**16**:
CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients.
See [CCM](/configuration/service/ccm).
**17**:
See [OCM](/configuration/service/ocm).
**18**:
Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile.
**19**:
Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0,
and only through a separate legacy build (with `-legacy-android-5` suffix).
For standalone binaries, the minimum Android version has been raised to Android 6.0,
since Termux requires Android 7.0 or later.
**20**:
This update fixes missing padding extension for Chrome 120+ fingerprints.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
use NaiveProxy instead for TLS fingerprint resistance.
#### 1.12.23
* Fixes and improvements
#### 1.13.0-rc.5
* Add `mipsle`, `mips64le`, `riscv64` and `loong64` support for NaiveProxy outbound
#### 1.12.22
* Fixes and improvements
#### 1.13.0-rc.3
* Fixes and improvements
#### 1.12.21
* Fixes and improvements
#### 1.13.0-rc.2
* Fixes and improvements
#### 1.12.20
* Fixes and improvements
#### 1.13.0-rc.1
* Fixes and improvements
#### 1.12.19
* Fixes and improvements
#### 1.13.0-beta.8
* Add fallback routing rule for `auto_redirect` **1**
* Fixes and improvements
**1**:
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
ensuring traffic is routed to the sing-box table when no route is found in system tables.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
#### 1.12.18
* Add fallback routing rule for `auto_redirect` **1**
* Fixes and improvements
**1**:
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
ensuring traffic is routed to the sing-box table when no route is found in system tables.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
#### 1.13.0-beta.6
* Update uTLS to v1.8.2 **1**
* Fixes and improvements
**1**:
This update fixes missing padding extension for Chrome 120+ fingerprints.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
use NaiveProxy instead for TLS fingerprint resistance.
#### 1.12.17
* Update uTLS to v1.8.2 **1**
* Fixes and improvements
**1**:
This update fixes missing padding extension for Chrome 120+ fingerprints.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
use NaiveProxy instead for TLS fingerprint resistance.
#### 1.13.0-beta.5
* Fixes and improvements
#### 1.12.16
* Fixes and improvements
#### 1.13.0-beta.4
* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs)
* Android: Add support for resisting VPN detection via Xposed
* Update quic-go to v0.59.0
* Fixes and improvements
#### 1.13.0-beta.2
* Add `bind_address_no_port` option for dial fields **1**

View File

@@ -2,11 +2,6 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -154,12 +149,6 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -419,26 +408,6 @@ Matches network interface (same values as `network_type`) address.
Match default interface address.
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### wifi_ssid
!!! quote ""

View File

@@ -2,11 +2,6 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -154,12 +149,6 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -418,26 +407,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配默认接口地址。
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### wifi_ssid
!!! quote ""

View File

@@ -8,8 +8,7 @@ icon: material/new-box
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
:material-plus: [system_interface](#system_interface)
:material-plus: [system_interface_name](#system_interface_name)
:material-plus: [system_interface_mtu](#system_interface_mtu)
:material-plus: [advertise_tags](#advertise_tags)
:material-plus: [system_interface_mtu](#system_interface_mtu)
!!! question "Since sing-box 1.12.0"
@@ -29,7 +28,6 @@ icon: material/new-box
"exit_node_allow_lan_access": false,
"advertise_routes": [],
"advertise_exit_node": false,
"advertise_tags": [],
"relay_server_port": 0,
"relay_server_static_endpoints": [],
"system_interface": false,
@@ -104,14 +102,6 @@ Example: `["192.168.1.1/24"]`
Indicates whether the node should advertise itself as an exit node.
#### advertise_tags
!!! question "Since sing-box 1.13.0"
Tags to advertise for this node, for ACL enforcement purposes.
Example: `["tag:server"]`
#### relay_server_port
!!! question "Since sing-box 1.13.0"

View File

@@ -8,8 +8,7 @@ icon: material/new-box
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
:material-plus: [system_interface](#system_interface)
:material-plus: [system_interface_name](#system_interface_name)
:material-plus: [system_interface_mtu](#system_interface_mtu)
:material-plus: [advertise_tags](#advertise_tags)
:material-plus: [system_interface_mtu](#system_interface_mtu)
!!! question "自 sing-box 1.12.0 起"
@@ -29,7 +28,6 @@ icon: material/new-box
"exit_node_allow_lan_access": false,
"advertise_routes": [],
"advertise_exit_node": false,
"advertise_tags": [],
"relay_server_port": 0,
"relay_server_static_endpoints": [],
"system_interface": false,
@@ -103,14 +101,6 @@ icon: material/new-box
指示节点是否应将自己通告为出口节点。
#### advertise_tags
!!! question "自 sing-box 1.13.0 起"
为此节点通告的标签,用于 ACL 执行。
示例:`["tag:server"]`
#### relay_server_port
!!! question "自 sing-box 1.13.0 起"

View File

@@ -2,21 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! 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)
:material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue)
:material-plus: [exclude_mptcp](#exclude_mptcp)
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
:material-plus: [exclude_mptcp](#exclude_mptcp)
!!! quote "Changes in sing-box 1.12.0"
@@ -81,7 +71,6 @@ icon: material/new-box
"auto_redirect_output_mark": "0x2024",
"auto_redirect_reset_mark": "0x2025",
"auto_redirect_nfqueue": 100,
"auto_redirect_iproute2_fallback_rule_index": 32768,
"exclude_mptcp": false,
"loopback_address": [
"10.7.0.1"
@@ -134,12 +123,6 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -320,17 +303,6 @@ NFQueue number used by `auto_redirect` pre-matching.
`100` is used by default.
#### auto_redirect_iproute2_fallback_rule_index
!!! question "Since sing-box 1.12.18"
Linux iproute2 fallback rule index generated by `auto_redirect`.
This rule is checked after system default rules (32766: main, 32767: default),
routing traffic to the sing-box table only when no route is found in system tables.
`32768` is used by default.
#### exclude_mptcp
!!! question "Since sing-box 1.13.0"
@@ -363,9 +335,6 @@ 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*:
@@ -566,30 +535,6 @@ Limit android packages in route.
Exclude android packages in route.
#### include_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Limit MAC addresses in route. Not limited by default.
Conflict with `exclude_mac_address`.
#### exclude_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Exclude MAC addresses in route.
Conflict with `include_mac_address`.
#### platform
Platform-specific settings, provided by client applications.

View File

@@ -2,21 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! 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)
:material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue)
:material-plus: [exclude_mptcp](#exclude_mptcp)
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
:material-plus: [exclude_mptcp](#exclude_mptcp)
!!! quote "sing-box 1.12.0 中的更改"
@@ -81,7 +71,6 @@ icon: material/new-box
"auto_redirect_output_mark": "0x2024",
"auto_redirect_reset_mark": "0x2025",
"auto_redirect_nfqueue": 100,
"auto_redirect_iproute2_fallback_rule_index": 32768,
"exclude_mptcp": false,
"loopback_address": [
"10.7.0.1"
@@ -135,12 +124,6 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -319,24 +302,13 @@ tun 接口的 IPv6 前缀。
默认使用 `100`
#### auto_redirect_iproute2_fallback_rule_index
!!! question "自 sing-box 1.12.18 起"
`auto_redirect` 生成的 iproute2 回退规则索引。
此规则在系统默认规则32766: main32767: default之后检查
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
默认使用 `32768`
#### exclude_mptcp
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux且需要 nftables`auto_route``auto_redirect` 已启用。
仅支持 Linux且需要 nftables`auto_route``auto_redirect` 已启用。
由于协议限制MPTCP 无法被透明代理。
@@ -362,9 +334,6 @@ tun 接口的 IPv6 前缀。
* 使不支持的网络不可达。
* 出于历史遗留原因,当未启用 `strict_route``auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量:
* 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。
* 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。
*在 Windows 中*
@@ -554,30 +523,6 @@ TCP/IP 栈。
排除路由的 Android 应用包名。
#### include_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
限制被路由的 MAC 地址。默认不限制。
`exclude_mac_address` 冲突。
#### exclude_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
排除路由的 MAC 地址。
`include_mac_address` 冲突。
#### platform
平台特定的设置,由客户端应用提供。

View File

@@ -34,12 +34,10 @@ icon: material/new-box
| Build Variant | Platforms | Description |
|---------------|-----------|-------------|
| (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.
| (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 |
**Runtime Requirements:**

View File

@@ -32,14 +32,12 @@ icon: material/new-box
**官方发布版本区别:**
| 构建变体 | 平台 | 说明 |
|---|---|---|
| (无后缀) | 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 变体。
| 构建变体 | 平台 | 说明 |
|-----------|------------------------|------------------------------------------|
| (默认) | 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` |
**运行时要求:**

View File

@@ -4,11 +4,6 @@ icon: material/alert-decagram
# Route
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [default_domain_resolver](#default_domain_resolver)
@@ -40,9 +35,6 @@ icon: material/alert-decagram
"override_android_vpn": false,
"default_interface": "",
"default_mark": 0,
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_domain_resolver": "", // or {}
"default_network_strategy": "",
"default_network_type": [],
@@ -115,38 +107,6 @@ Set routing mark by default.
Takes no effect if `outbound.routing_mark` is set.
#### find_process
!!! quote ""
Only supported on Linux, Windows, and macOS.
Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist.
#### find_neighbor
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux and macOS.
Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist.
See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
#### dhcp_lease_files
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux and macOS.
Custom DHCP lease file paths for hostname and MAC address resolution.
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
#### default_domain_resolver
!!! question "Since sing-box 1.12.0"

View File

@@ -4,11 +4,6 @@ icon: material/alert-decagram
# 路由
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [default_domain_resolver](#default_domain_resolver)
@@ -42,9 +37,6 @@ icon: material/alert-decagram
"override_android_vpn": false,
"default_interface": "",
"default_mark": 0,
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_network_strategy": "",
"default_fallback_delay": ""
}
@@ -114,38 +106,6 @@ icon: material/alert-decagram
如果设置了 `outbound.routing_mark` 设置,则不生效。
#### find_process
!!! quote ""
仅支持 Linux、Windows 和 macOS。
在没有 `process_name``process_path``package_name``user``user_id` 规则时启用进程搜索以输出日志。
#### find_neighbor
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux 和 macOS。
在没有 `source_mac_address``source_hostname` 规则时启用邻居解析以输出日志。
参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
#### dhcp_lease_files
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux 和 macOS。
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
为空时自动从常见 DHCP 服务器dnsmasq、odhcpd、ISC dhcpd、Kea检测。
#### default_domain_resolver
!!! question "自 sing-box 1.12.0 起"

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -164,12 +159,6 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -460,26 +449,6 @@ Match specified outbounds' preferred routes.
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### rule_set
!!! question "Since sing-box 1.8.0"

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -161,12 +156,6 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -457,26 +446,6 @@ icon: material/new-box
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
| `wireguard` | 匹配对端的 allowed IPs |
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### rule_set
!!! question "自 sing-box 1.8.0 起"

View File

@@ -10,11 +10,6 @@ CCM (Claude Code Multiplexer) service is a multiplexing service that allows you
It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable.
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### Structure
```json
@@ -24,7 +19,6 @@ It handles OAuth authentication with Claude's API on your local machine while al
... // Listen Fields
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -51,77 +45,6 @@ On macOS, credentials are read from the system keychain first, then fall back to
Refreshed tokens are automatically written back to the same location.
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
On macOS without an explicit `credential_path`, keychain changes are not watched. Automatic reload only applies to the credential file path.
Conflict with `credentials`.
#### credentials
!!! question "Since sing-box 1.14.0"
List of credential configurations for multi-credential mode.
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
Each credential has a `type` field (`default`, `balancer`, or `fallback`) and a required `tag` field.
##### Default Credential
```json
{
"tag": "a",
"credential_path": "/path/to/.credentials.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
- `usages_path`: Optional usage tracking file for this credential.
- `detour`: Outbound tag for connecting to the Claude API with this credential.
- `reserve_5h`: Reserve threshold (1-99) for 5-hour window. Credential pauses at (100-N)% utilization.
- `reserve_weekly`: Reserve threshold (1-99) for weekly window. Credential pauses at (100-N)% utilization.
##### Balancer Credential
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random`. `least_used` will be used by default.
- `credentials`: ==Required== List of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
##### Fallback Credential
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
Uses credentials in order. Falls through to the next when the current one is exhausted.
- `credentials`: ==Required== Ordered list of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
#### usages_path
Path to the file for storing aggregated API usage statistics.
@@ -137,29 +60,13 @@ Statistics are organized by model, context window (200k standard vs 1M premium),
The statistics file is automatically saved every minute and upon service shutdown.
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
#### users
List of authorized users for token authentication.
If empty, no authentication is required.
Object format:
```json
{
"name": "",
"token": "",
"credential": ""
}
```
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.
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
#### headers
@@ -171,93 +78,29 @@ These headers will override any existing headers with the same name.
Outbound tag for connecting to the Claude API.
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
### Example
#### Server
```json
{
"services": [
{
"type": "ccm",
"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"
}
]
"listen": "127.0.0.1",
"listen_port": 8080
}
]
}
```
#### Client
Connect to the CCM service:
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
claude
```
### Example with Multiple Credentials
#### Server
```json
{
"services": [
{
"type": "ccm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.claude-a/.credentials.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.claude-b/.credentials.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -10,11 +10,6 @@ CCMClaude Code 多路复用器)服务是一个多路复用服务,允许
它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### 结构
```json
@@ -24,7 +19,6 @@ CCMClaude Code 多路复用器)服务是一个多路复用服务,允许
... // 监听字段
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -51,77 +45,6 @@ Claude Code OAuth 凭据文件的路径。
刷新的令牌会自动写回相同位置。
`credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
在 macOS 上如果未显式设置 `credential_path`,不会监听钥匙串变化。自动重载只作用于凭据文件路径。
`credentials` 冲突。
#### credentials
!!! question "自 sing-box 1.14.0 起"
多凭据模式的凭据配置列表。
设置后,顶层 `credential_path``usages_path``detour` 被禁止。每个用户必须指定 `credential` 标签。
每个凭据有一个 `type` 字段(`default``balancer``fallback`)和一个必填的 `tag` 字段。
##### 默认凭据
```json
{
"tag": "a",
"credential_path": "/path/to/.credentials.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
- `usages_path`:此凭据的可选使用跟踪文件。
- `detour`:此凭据用于连接 Claude API 的出站标签。
- `reserve_5h`5 小时窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
- `reserve_weekly`每周窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
##### 均衡凭据
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random`。默认使用 `least_used`
- `credentials`==必填== 默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
##### 回退凭据
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
按顺序使用凭据。当前凭据耗尽后切换到下一个。
- `credentials`==必填== 有序的默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
@@ -137,29 +60,13 @@ Claude Code OAuth 凭据文件的路径。
统计文件每分钟自动保存一次,并在服务关闭时保存。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`
#### users
用于令牌身份验证的授权用户列表。
如果为空,则不需要身份验证。
对象格式:
```json
{
"name": "",
"token": "",
"credential": ""
}
```
对象字段:
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
#### headers
@@ -171,93 +78,29 @@ Claude Code OAuth 凭据文件的路径。
用于连接 Claude API 的出站标签。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
### 示例
#### 服务端
```json
{
"services": [
{
"type": "ccm",
"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"
}
]
"listen": "127.0.0.1",
"listen_port": 8080
}
]
}
```
#### 客户端
连接到 CCM 服务:
```bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
claude
```
### 多凭据示例
#### 服务端
```json
{
"services": [
{
"type": "ccm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.claude-a/.credentials.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.claude-b/.credentials.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -10,11 +10,6 @@ OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you
It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens.
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### Structure
```json
@@ -24,7 +19,6 @@ It handles OAuth authentication with OpenAI's API on your local machine while al
... // Listen Fields
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -43,81 +37,10 @@ See [Listen Fields](/configuration/shared/listen/) for details.
Path to the OpenAI OAuth credentials file.
If not specified, defaults to:
- `$CODEX_HOME/auth.json` if `CODEX_HOME` environment variable is set
- `~/.codex/auth.json` otherwise
If not specified, defaults to `~/.codex/auth.json`.
Refreshed tokens are automatically written back to the same location.
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
Conflict with `credentials`.
#### credentials
!!! question "Since sing-box 1.14.0"
List of credential configurations for multi-credential mode.
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
Each credential has a `type` field (`default`, `balancer`, or `fallback`) and a required `tag` field.
##### Default Credential
```json
{
"tag": "a",
"credential_path": "/path/to/auth.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
- `usages_path`: Optional usage tracking file for this credential.
- `detour`: Outbound tag for connecting to the OpenAI API with this credential.
- `reserve_5h`: Reserve threshold (1-99) for primary rate limit window. Credential pauses at (100-N)% utilization.
- `reserve_weekly`: Reserve threshold (1-99) for secondary (weekly) rate limit window. Credential pauses at (100-N)% utilization.
##### Balancer Credential
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random`. `least_used` will be used by default.
- `credentials`: ==Required== List of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
##### Fallback Credential
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
Uses credentials in order. Falls through to the next when the current one is exhausted.
- `credentials`: ==Required== Ordered list of default credential tags.
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
#### usages_path
Path to the file for storing aggregated API usage statistics.
@@ -133,8 +56,6 @@ Statistics are organized by model and optionally by user when authentication is
The statistics file is automatically saved every minute and upon service shutdown.
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
#### users
List of authorized users for token authentication.
@@ -146,8 +67,7 @@ Object format:
```json
{
"name": "",
"token": "",
"credential": ""
"token": ""
}
```
@@ -155,7 +75,6 @@ Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer <token>` header.
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
#### headers
@@ -167,8 +86,6 @@ These headers will override any existing headers with the same name.
Outbound tag for connecting to the OpenAI API.
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
@@ -194,23 +111,17 @@ 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"
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"
wire_api = "responses"
requires_openai_auth = false
```
Then run:
```bash
codex --profile ocm
codex --model-provider ocm
```
### Example with Authentication
@@ -228,11 +139,11 @@ codex --profile ocm
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world"
"token": "sk-alice-secret-token"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob"
"token": "sk-bob-secret-token"
}
]
}
@@ -245,71 +156,16 @@ codex --profile 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"
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"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "sk-alice-secret-token"
```
Then run:
```bash
codex --profile ocm
```
### Example with Multiple Credentials
#### Server
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.codex-a/auth.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.codex-b/auth.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob",
"credential": "a"
}
]
}
]
}
codex --model-provider ocm
```

View File

@@ -10,11 +10,6 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [credentials](#credentials)
:material-alert: [users](#users)
### 结构
```json
@@ -24,7 +19,6 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
... // 监听字段
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -43,81 +37,10 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
OpenAI OAuth 凭据文件的路径。
如果未指定,默认值为
- 如果设置了 `CODEX_HOME` 环境变量,则使用 `$CODEX_HOME/auth.json`
- 否则使用 `~/.codex/auth.json`
如果未指定,默认值为 `~/.codex/auth.json`
刷新的令牌会自动写回相同位置。
`credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
`credentials` 冲突。
#### credentials
!!! question "自 sing-box 1.14.0 起"
多凭据模式的凭据配置列表。
设置后,顶层 `credential_path``usages_path``detour` 被禁止。每个用户必须指定 `credential` 标签。
每个凭据有一个 `type` 字段(`default``balancer``fallback`)和一个必填的 `tag` 字段。
##### 默认凭据
```json
{
"tag": "a",
"credential_path": "/path/to/auth.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20
}
```
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
- `usages_path`:此凭据的可选使用跟踪文件。
- `detour`:此凭据用于连接 OpenAI API 的出站标签。
- `reserve_5h`主要速率限制窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
- `reserve_weekly`次要每周速率限制窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。
##### 均衡凭据
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"],
"poll_interval": "60s"
}
```
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random`。默认使用 `least_used`
- `credentials`==必填== 默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
##### 回退凭据
```json
{
"tag": "backup",
"type": "fallback",
"credentials": ["a", "b"],
"poll_interval": "30s"
}
```
按顺序使用凭据。当前凭据耗尽后切换到下一个。
- `credentials`==必填== 有序的默认凭据标签列表。
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
@@ -133,8 +56,6 @@ OpenAI OAuth 凭据文件的路径。
统计文件每分钟自动保存一次,并在服务关闭时保存。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`
#### users
用于令牌身份验证的授权用户列表。
@@ -146,8 +67,7 @@ OpenAI OAuth 凭据文件的路径。
```json
{
"name": "",
"token": "",
"credential": ""
"token": ""
}
```
@@ -155,7 +75,6 @@ OpenAI OAuth 凭据文件的路径。
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer <token>` 头进行身份验证。
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
#### headers
@@ -167,8 +86,6 @@ OpenAI OAuth 凭据文件的路径。
用于连接 OpenAI API 的出站标签。
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
@@ -194,24 +111,17 @@ 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"
supports_websockets = true
[profiles.ocm]
model_provider = "ocm"
# model = "gpt-5.4" # 如果最新模型尚未公开发布
# model_reasoning_effort = "xhigh"
wire_api = "responses"
requires_openai_auth = false
```
然后运行:
```bash
codex --profile ocm
codex --model-provider ocm
```
### 带身份验证的示例
@@ -229,11 +139,11 @@ codex --profile ocm
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world"
"token": "sk-alice-secret-token"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob"
"token": "sk-bob-secret-token"
}
]
}
@@ -246,71 +156,16 @@ codex --profile ocm
`~/.codex/config.toml` 中添加:
```toml
# profile = "ocm" # 设为默认配置
[model_providers.ocm]
name = "OCM Proxy"
base_url = "http://127.0.0.1:8080/v1"
supports_websockets = true
experimental_bearer_token = "sk-ocm-hello-world"
[profiles.ocm]
model_provider = "ocm"
# model = "gpt-5.4" # 如果最新模型尚未公开发布
# model_reasoning_effort = "xhigh"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "sk-alice-secret-token"
```
然后运行:
```bash
codex --profile ocm
```
### 多凭据示例
#### 服务端
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.codex-a/auth.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.codex-b/auth.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"poll_interval": "60s",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob",
"credential": "a"
}
]
}
]
}
codex --model-provider ocm
```

View File

@@ -5,8 +5,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [alidns.security_token](#security_token)
:material-plus: [cloudflare.zone_token](#zone_token)
:material-plus: [acmedns](#acmedns)
:material-plus: [cloudflare.zone_token](#zone_token)
### Structure
@@ -55,19 +54,3 @@ The Security Token for STS temporary credentials.
Optional API token with `Zone:Read` permission.
When provided, allows `api_token` to be scoped to a single zone.
#### ACME-DNS
!!! question "Since sing-box 1.13.0"
```json
{
"provider": "acmedns",
"username": "",
"password": "",
"subdomain": "",
"server_url": ""
}
```
See [ACME-DNS](https://github.com/joohoi/acme-dns) for details.

View File

@@ -5,8 +5,7 @@ icon: material/new-box
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [alidns.security_token](#security_token)
:material-plus: [cloudflare.zone_token](#zone_token)
:material-plus: [acmedns](#acmedns)
:material-plus: [cloudflare.zone_token](#zone_token)
### 结构
@@ -55,19 +54,3 @@ icon: material/new-box
具有 `Zone:Read` 权限的可选 API 令牌。
提供后可将 `api_token` 限定到单个区域。
#### ACME-DNS
!!! question "自 sing-box 1.13.0 起"
```json
{
"provider": "acmedns",
"username": "",
"password": "",
"subdomain": "",
"server_url": ""
}
```
参阅 [ACME-DNS](https://github.com/joohoi/acme-dns)。

View File

@@ -1,49 +0,0 @@
---
icon: material/lan
---
# Neighbor Resolution
Match LAN devices by MAC address and hostname using
[`source_mac_address`](/configuration/route/rule/#source_mac_address) and
[`source_hostname`](/configuration/route/rule/#source_hostname) rule items.
Neighbor resolution is automatically enabled when these rule items exist.
Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules.
## Linux
Works natively. No special setup required.
Hostname resolution requires DHCP lease files,
automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea).
Custom paths can be set via [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files).
## Android
!!! quote ""
Only supported in graphical clients.
Requires Android 11 or above and ROOT.
Must use [VPNHotspot](https://github.com/Mygod/VPNHotspot) to share the VPN connection.
ROM built-in features like "Use VPN for connected devices" can share VPN
but cannot provide MAC address or hostname information.
Set **IP Masquerade Mode** to **None** in VPNHotspot settings.
Only route/DNS rules are supported. TUN include/exclude routes are not supported.
### Hostname Visibility
Hostname is only visible in sing-box if it is visible in VPNHotspot.
For Apple devices, change **Private Wi-Fi Address** from **Rotating** to **Fixed** in the Wi-Fi settings
of the connected network. Non-Apple devices are always visible.
## macOS
Requires the standalone version (macOS system extension).
The App Store version can share the VPN as a hotspot but does not support MAC address or hostname reading.
See [VPN Hotspot](/manual/misc/vpn-hotspot/#macos) for Internet Sharing setup.

View File

@@ -1,49 +0,0 @@
---
icon: material/lan
---
# 邻居解析
通过
[`source_mac_address`](/configuration/route/rule/#source_mac_address) 和
[`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。
当这些规则项存在时,邻居解析自动启用。
使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。
## Linux
原生支持,无需特殊设置。
主机名解析需要 DHCP 租约文件,
自动从常见 DHCP 服务器dnsmasq、odhcpd、ISC dhcpd、Kea检测。
可通过 [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files) 设置自定义路径。
## Android
!!! quote ""
仅在图形客户端中支持。
需要 Android 11 或以上版本和 ROOT。
必须使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot) 共享 VPN 连接。
ROM 自带的「通过 VPN 共享连接」等功能可以共享 VPN
但无法提供 MAC 地址或主机名信息。
在 VPNHotspot 设置中将 **IP 遮掩模式** 设为 **无**
仅支持路由/DNS 规则。不支持 TUN 的 include/exclude 路由。
### 设备可见性
MAC 地址和主机名仅在 VPNHotspot 中可见时 sing-box 才能读取。
对于 Apple 设备,需要在所连接网络的 Wi-Fi 设置中将**私有无线局域网地址**从**轮替**改为**固定**。
非 Apple 设备始终可见。
## macOS
需要独立版本macOS 系统扩展)。
App Store 版本可以共享 VPN 热点但不支持 MAC 地址或主机名读取。
参阅 [VPN 热点](/manual/misc/vpn-hotspot/#macos) 了解互联网共享设置。

View File

@@ -418,18 +418,9 @@ Enable kernel TLS receive support.
==Client only==
!!! failure "Not Recommended"
uTLS has had repeated fingerprinting vulnerabilities discovered by researchers.
uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying
ClientHello structure. However, browsers use completely different TLS stacks
(Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors
that cannot be replicated by simply copying the handshake format, making detection possible.
Additionally, the library lacks active maintenance and has poor code quality,
making it unsuitable for censorship circumvention.
For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead.
!!! failure ""
There is no evidence that GFW detects and blocks servers based on TLS client fingerprinting, and using an imperfect emulation that has not been security reviewed could pose security risks.
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.

View File

@@ -417,16 +417,9 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
==仅客户端==
!!! failure "不推荐"
!!! failure ""
uTLS 已被研究人员多次发现其指纹可被识别的漏洞
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
然而,浏览器使用完全不同的 TLS 实现Chrome 使用 BoringSSLFirefox 使用 NSS
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。

View File

@@ -57,49 +57,25 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale). |
| `with_ccm` | :material-check: | Build with Claude Code Multiplexer service support. |
| `with_ocm` | :material-check: | Build with OpenAI Codex Multiplexer service support. |
| `with_naive_outbound` | :material-check: | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). |
| `badlinkname` | :material-check: | Enable `go:linkname` access to internal standard library functions. Required because the Go standard library does not expose many low-level APIs needed by this project, and reimplementing them externally is impractical. Used for kTLS (kernel TLS offload) and raw TLS record manipulation. |
| `tfogo_checklinkname0` | :material-check: | Companion to `badlinkname`. Go 1.23+ enforces `go:linkname` restrictions via the linker; this tag signals the build uses `-checklinkname=0` to bypass that enforcement. |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
| `with_naive_outbound` | :material-close: | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). |
It is not recommended to change the default build tag list unless you really know what you are adding.
## :material-wrench: Linker Flags
The following `-ldflags` are used in official builds:
| Flag | Description |
|-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 enabled Multipath TCP for listeners by default (`multipathtcp=2`). This may cause errors on low-level sockets, and sing-box has its own MPTCP control (`tcp_multi_path` option). This flag disables the Go default. |
| `-checklinkname=0` | Go 1.23+ linker rejects unauthorized `go:linkname` usage. This flag disables the check, required together with the `badlinkname` build tag. |
## :material-package-variant: For Downstream Packagers
The default build tag lists and linker flags are available as files in the repository for downstream packagers to reference directly:
| File | Description |
|------|-------------|
| `release/DEFAULT_BUILD_TAGS` | Default for Linux (common architectures), Darwin, and Android. |
| `release/DEFAULT_BUILD_TAGS_WINDOWS` | Default for Windows (includes `with_purego`). |
| `release/DEFAULT_BUILD_TAGS_OTHERS` | Default for other platforms (no `with_naive_outbound`). |
| `release/LDFLAGS` | Required linker flags (see above). |
## :material-layers: with_naive_outbound
NaiveProxy outbound requires special build configurations depending on your target platform.
### Supported Platforms
| 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 |
| 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 |
### Windows

View File

@@ -61,49 +61,25 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/configuration/endpoint/tailscale) |
| `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 |
| `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 |
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/configuration/outbound/naive/)。 |
| `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API且在外部重新实现不切实际。用于 kTLS内核 TLS 卸载)和原始 TLS 记录操作。 |
| `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
| `with_naive_outbound` | :material-close: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 |
除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。
## :material-wrench: 链接器标志
以下 `-ldflags` 在官方构建中使用:
| 标志 | 说明 |
|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 默认为监听器启用 Multipath TCP`multipathtcp=2`)。这可能在底层 socket 上导致错误,且 sing-box 有自己的 MPTCP 控制(`tcp_multi_path` 选项)。此标志禁用 Go 的默认行为。 |
| `-checklinkname=0` | Go 1.23+ 链接器拒绝未授权的 `go:linkname` 使用。此标志禁用该检查,需要与 `badlinkname` 构建标记一起使用。 |
## :material-package-variant: 下游打包者
默认构建标签列表和链接器标志以文件形式存放在仓库中,供下游打包者直接引用:
| 文件 | 说明 |
|------|------|
| `release/DEFAULT_BUILD_TAGS` | Linux常见架构、Darwin 和 Android 的默认标签。 |
| `release/DEFAULT_BUILD_TAGS_WINDOWS` | Windows 的默认标签(包含 `with_purego`)。 |
| `release/DEFAULT_BUILD_TAGS_OTHERS` | 其他平台的默认标签(不含 `with_naive_outbound`)。 |
| `release/LDFLAGS` | 必需的链接器标志(参见上文)。 |
## :material-layers: with_naive_outbound
NaiveProxy 出站需要根据目标平台进行特殊的构建配置。
### 支持的平台
| 平台 | 架构 | 模式 | 要求 |
|--------------|----------------------------------------------------------|--------|-----------------------------------------------------|
| 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 |
| 平台 | 架构 | 模式 | 要求 |
|---------------|------------------------|--------|--------------------------------|
| 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 |
### Windows

View File

@@ -47,17 +47,6 @@ 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

@@ -4,7 +4,8 @@ icon: material/horse
# Trojan
Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations.
Torjan is the most commonly used TLS proxy made in China. It can be used in various combinations,
but only the combination of uTLS and multiplexing is recommended.
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
@@ -139,7 +140,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
"password": "password",
"tls": {
"enabled": true,
"server_name": "example.org"
"server_name": "example.org",
"utls": {
"enabled": true,
"fingerprint": "firefox"
}
},
"multiplex": {
"enabled": true
@@ -166,7 +171,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
"tls": {
"enabled": true,
"server_name": "example.org",
"certificate_path": "/path/to/certificate.pem"
"certificate_path": "/path/to/certificate.pem",
"utls": {
"enabled": true,
"fingerprint": "firefox"
}
},
"multiplex": {
"enabled": true
@@ -189,7 +198,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
"tls": {
"enabled": true,
"server_name": "example.org",
"insecure": true
"insecure": true,
"utls": {
"enabled": true,
"fingerprint": "firefox"
}
},
"multiplex": {
"enabled": true

View File

@@ -45,7 +45,6 @@ type CacheFile struct {
storeRDRC bool
rdrcTimeout time.Duration
DB *bbolt.DB
resetAccess sync.Mutex
saveMetadataTimer *time.Timer
saveFakeIPAccess sync.RWMutex
saveDomain map[netip.Addr]string
@@ -170,55 +169,13 @@ func (c *CacheFile) Close() error {
return c.DB.Close()
}
func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) {
defer func() {
if r := recover(); r != nil {
c.resetDB()
err = E.New("database corrupted: ", r)
}
}()
return c.DB.View(fn)
}
func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) {
defer func() {
if r := recover(); r != nil {
c.resetDB()
err = E.New("database corrupted: ", r)
}
}()
return c.DB.Batch(fn)
}
func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) {
defer func() {
if r := recover(); r != nil {
c.resetDB()
err = E.New("database corrupted: ", r)
}
}()
return c.DB.Update(fn)
}
func (c *CacheFile) resetDB() {
c.resetAccess.Lock()
defer c.resetAccess.Unlock()
c.DB.Close()
os.Remove(c.path)
db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second})
if err == nil {
_ = filemanager.Chown(c.ctx, c.path)
c.DB = db
}
}
func (c *CacheFile) StoreFakeIP() bool {
return c.storeFakeIP
}
func (c *CacheFile) LoadMode() string {
var mode string
c.view(func(t *bbolt.Tx) error {
c.DB.View(func(t *bbolt.Tx) error {
bucket := t.Bucket(bucketMode)
if bucket == nil {
return nil
@@ -236,7 +193,7 @@ func (c *CacheFile) LoadMode() string {
}
func (c *CacheFile) StoreMode(mode string) error {
return c.batch(func(t *bbolt.Tx) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketMode)
if err != nil {
return err
@@ -273,7 +230,7 @@ func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error)
func (c *CacheFile) LoadSelected(group string) string {
var selected string
c.view(func(t *bbolt.Tx) error {
c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketSelected)
if bucket == nil {
return nil
@@ -288,7 +245,7 @@ func (c *CacheFile) LoadSelected(group string) string {
}
func (c *CacheFile) StoreSelected(group, selected string) error {
return c.batch(func(t *bbolt.Tx) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketSelected)
if err != nil {
return err
@@ -298,7 +255,7 @@ func (c *CacheFile) StoreSelected(group, selected string) error {
}
func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
c.view(func(t *bbolt.Tx) error {
c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketExpand)
if bucket == nil {
return nil
@@ -314,7 +271,7 @@ func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
}
func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
return c.batch(func(t *bbolt.Tx) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketExpand)
if err != nil {
return err
@@ -329,7 +286,7 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
var savedSet adapter.SavedBinary
err := c.view(func(t *bbolt.Tx) error {
err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketRuleSet)
if bucket == nil {
return os.ErrNotExist
@@ -347,7 +304,7 @@ func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
}
func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
return c.batch(func(t *bbolt.Tx) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketRuleSet)
if err != nil {
return err

View File

@@ -23,7 +23,7 @@ var (
func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
var metadata adapter.FakeIPMetadata
err := c.batch(func(tx *bbolt.Tx) error {
err := c.DB.Batch(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return os.ErrNotExist
@@ -45,7 +45,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
}
func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
return c.batch(func(tx *bbolt.Tx) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
@@ -69,7 +69,7 @@ func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) {
}
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
return c.batch(func(tx *bbolt.Tx) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
@@ -136,7 +136,7 @@ func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
return cachedDomain, true
}
var domain string
_ = c.view(func(tx *bbolt.Tx) error {
_ = c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return nil
@@ -163,7 +163,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
return cachedAddress, true
}
var address netip.Addr
_ = c.view(func(tx *bbolt.Tx) error {
_ = c.DB.View(func(tx *bbolt.Tx) error {
var bucket *bbolt.Bucket
if isIPv6 {
bucket = tx.Bucket(bucketFakeIPDomain6)
@@ -180,7 +180,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
}
func (c *CacheFile) FakeIPReset() error {
return c.batch(func(tx *bbolt.Tx) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
err := tx.DeleteBucket(bucketFakeIP)
if err != nil {
return err

View File

@@ -31,7 +31,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
copy(key[2:], qName)
defer buf.Put(key)
var deleteCache bool
err := c.view(func(tx *bbolt.Tx) error {
err := c.DB.View(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketRDRC)
if bucket == nil {
return nil
@@ -56,7 +56,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
return
}
if deleteCache {
c.update(func(tx *bbolt.Tx) error {
c.DB.Update(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketRDRC)
if bucket == nil {
return nil
@@ -72,7 +72,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
}
func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error {
return c.batch(func(tx *bbolt.Tx) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := c.createBucket(tx, bucketRDRC)
if err != nil {
return err

View File

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

View File

@@ -2,7 +2,6 @@ package clashapi
import (
"bytes"
"context"
"net/http"
"strconv"
"time"
@@ -18,15 +17,15 @@ import (
"github.com/gofrs/uuid/v5"
)
func connectionRouter(ctx context.Context, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
r := chi.NewRouter()
r.Get("/", getConnections(ctx, trafficManager))
r.Get("/", getConnections(trafficManager))
r.Delete("/", closeAllConnections(router, trafficManager))
r.Delete("/{id}", closeConnection(trafficManager))
return r
}
func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") != "websocket" {
snapshot := trafficManager.Snapshot()
@@ -38,7 +37,6 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
if err != nil {
return
}
defer conn.Close()
intervalStr := r.URL.Query().Get("interval")
interval := 1000
@@ -69,12 +67,7 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
for range tick.C {
if err = sendSnapshot(); err != nil {
break
}

View File

@@ -115,13 +115,13 @@ 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(s.ctx, logFactory))
r.Get("/traffic", traffic(s.ctx, trafficManager))
r.Get("/logs", getLogs(logFactory))
r.Get("/traffic", traffic(trafficManager))
r.Get("/version", version)
r.Mount("/configs", configRouter(s, logFactory))
r.Mount("/proxies", proxyRouter(s, s.router))
r.Mount("/rules", ruleRouter(s.router))
r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager))
r.Mount("/connections", connectionRouter(s.router, trafficManager))
r.Mount("/providers/proxies", proxyProviderRouter())
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
@@ -303,7 +303,7 @@ type Traffic struct {
Down int64 `json:"down"`
}
func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
func traffic(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" {
@@ -324,12 +324,7 @@ func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w
defer tick.Stop()
buf := &bytes.Buffer{}
uploadTotal, downloadTotal := trafficManager.Total()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
for range tick.C {
buf.Reset()
uploadTotalNew, downloadTotalNew := trafficManager.Total()
err := json.NewEncoder(buf).Encode(Traffic{
@@ -360,7 +355,7 @@ type Log struct {
Payload string `json:"payload"`
}
func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
func getLogs(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,8 +394,6 @@ func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.
var logEntry log.Entry
for {
select {
case <-ctx.Done():
return
case <-done:
return
case logEntry = <-subscription:

View File

@@ -10,31 +10,11 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/x/list"
"github.com/gofrs/uuid/v5"
)
type ConnectionEventType int
const (
ConnectionEventNew ConnectionEventType = iota
ConnectionEventUpdate
ConnectionEventClosed
)
type ConnectionEvent struct {
Type ConnectionEventType
ID uuid.UUID
Metadata *TrackerMetadata
UplinkDelta int64
DownlinkDelta int64
ClosedAt time.Time
}
const closedConnectionsLimit = 1000
type Manager struct {
uploadTotal atomic.Int64
downloadTotal atomic.Int64
@@ -42,52 +22,29 @@ type Manager struct {
connections compatible.Map[uuid.UUID, Tracker]
closedConnectionsAccess sync.Mutex
closedConnections list.List[TrackerMetadata]
memory uint64
eventSubscriber *observable.Subscriber[ConnectionEvent]
// process *process.Process
memory uint64
}
func NewManager() *Manager {
return &Manager{}
}
func (m *Manager) SetEventHook(subscriber *observable.Subscriber[ConnectionEvent]) {
m.eventSubscriber = subscriber
}
func (m *Manager) Join(c Tracker) {
metadata := c.Metadata()
m.connections.Store(metadata.ID, c)
if m.eventSubscriber != nil {
m.eventSubscriber.Emit(ConnectionEvent{
Type: ConnectionEventNew,
ID: metadata.ID,
Metadata: metadata,
})
}
m.connections.Store(c.Metadata().ID, c)
}
func (m *Manager) Leave(c Tracker) {
metadata := c.Metadata()
_, loaded := m.connections.LoadAndDelete(metadata.ID)
if loaded {
closedAt := time.Now()
metadata.ClosedAt = closedAt
metadataCopy := *metadata
metadata.ClosedAt = time.Now()
m.closedConnectionsAccess.Lock()
if m.closedConnections.Len() >= closedConnectionsLimit {
defer m.closedConnectionsAccess.Unlock()
if m.closedConnections.Len() >= 1000 {
m.closedConnections.PopFront()
}
m.closedConnections.PushBack(metadataCopy)
m.closedConnectionsAccess.Unlock()
if m.eventSubscriber != nil {
m.eventSubscriber.Emit(ConnectionEvent{
Type: ConnectionEventClosed,
ID: metadata.ID,
Metadata: &metadataCopy,
ClosedAt: closedAt,
})
}
m.closedConnections.PushBack(metadata)
}
}
@@ -107,8 +64,8 @@ func (m *Manager) ConnectionsLen() int {
return m.connections.Len()
}
func (m *Manager) Connections() []*TrackerMetadata {
var connections []*TrackerMetadata
func (m *Manager) Connections() []TrackerMetadata {
var connections []TrackerMetadata
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
connections = append(connections, value.Metadata())
return true
@@ -116,18 +73,10 @@ func (m *Manager) Connections() []*TrackerMetadata {
return connections
}
func (m *Manager) ClosedConnections() []*TrackerMetadata {
func (m *Manager) ClosedConnections() []TrackerMetadata {
m.closedConnectionsAccess.Lock()
values := m.closedConnections.Array()
m.closedConnectionsAccess.Unlock()
if len(values) == 0 {
return nil
}
connections := make([]*TrackerMetadata, len(values))
for i := range values {
connections[i] = &values[i]
}
return connections
defer m.closedConnectionsAccess.Unlock()
return m.closedConnections.Array()
}
func (m *Manager) Connection(id uuid.UUID) Tracker {
@@ -175,7 +124,7 @@ func (s *Snapshot) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"downloadTotal": s.Download,
"uploadTotal": s.Upload,
"connections": common.Map(s.Connections, func(t Tracker) *TrackerMetadata { return t.Metadata() }),
"connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }),
"memory": s.Memory,
})
}

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