Compare commits

..

117 Commits

Author SHA1 Message Date
世界
7a04aff02c documentation: Bump version 2025-12-30 18:24:33 +08:00
世界
97e20090cb cronet: Fix windows DNS hijack 2025-12-30 18:24:33 +08:00
世界
3bbb23ea3b Fix DNS transports 2025-12-30 18:24:33 +08:00
世界
7da7601903 platform: Expose process info 2025-12-30 18:24:33 +08:00
世界
eaee6bc493 Update bypass action behavior for auto redirect 2025-12-30 18:24:33 +08:00
世界
b34469986b platform: Add GetStartedAt for StartedService 2025-12-29 13:31:36 +08:00
世界
0a88fc6314 documentation: Format changes header 2025-12-26 16:30:32 +08:00
世界
e984ad753b Add format_docs command for documentation trailing space formatting 2025-12-26 16:30:32 +08:00
世界
88bda5c306 Fix panic when closing Box before Start with file log output 2025-12-26 16:30:32 +08:00
世界
3294a46a28 Add pre-match support for auto redirect 2025-12-26 16:30:32 +08:00
世界
ce285f8d79 Fix cronet on iOS 2025-12-26 11:49:49 +08:00
世界
4e73574144 documentation: Bump version 2025-12-25 16:43:00 +08:00
世界
8881882326 Ignore darwin IP_DONTFRAG error when not supported 2025-12-25 16:43:00 +08:00
世界
b675ed2563 Update tailscale to v1.92.4 2025-12-25 16:43:00 +08:00
世界
a44f8c7b5d Update cronet-go to v143.0.7499.109-1 2025-12-25 16:43:00 +08:00
世界
003cf13898 platform: Split library for Android SDK 21 and 23 2025-12-25 16:43:00 +08:00
世界
2f906adfa1 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2025-12-25 14:51:59 +08:00
世界
bef0a2f240 documentation: Minor fixes 2025-12-25 14:51:59 +08:00
世界
e27a335ee0 documentation: Add Wi-Fi state shared page 2025-12-25 14:51:59 +08:00
世界
a1694d4c7b Fix missing build constraints for linux wifi state monitor 2025-12-25 14:51:59 +08:00
世界
48e5344cea Update dependencies 2025-12-25 14:51:59 +08:00
世界
6e59a76941 Update quic-go to v0.58.0 2025-12-25 14:51:59 +08:00
世界
6a7264aa91 Add Chrome Root Store certificate option
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
2025-12-25 14:51:59 +08:00
世界
bc23473411 Fix cronet-go crash 2025-12-25 14:51:59 +08:00
世界
8171e792cc Add trace logging for lifecycle calls
Log start/close operations with timing information for debugging.
2025-12-25 14:51:59 +08:00
世界
589e4e5bd7 documentation: Minor fixes 2025-12-25 14:51:59 +08:00
世界
7dd91362b5 Remove certificate_public_key_sha256 for naive 2025-12-25 14:51:59 +08:00
世界
0e79256f15 platform: Use new crash log api 2025-12-25 14:51:59 +08:00
世界
05169b09ad Fix naive network 2025-12-25 14:51:59 +08:00
世界
99c125d8f3 Add QUIC support for naiveproxy 2025-12-25 14:51:59 +08:00
世界
e473c64cd6 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
2025-12-25 14:51:59 +08:00
世界
be7254c335 Fix naiveproxy build 2025-12-25 14:51:58 +08:00
世界
bf0e432340 Add OpenAI Codex Multiplexer service 2025-12-25 14:51:58 +08:00
世界
d592e2d12a Update pricing for CCM service 2025-12-25 14:51:58 +08:00
世界
dd164d9150 release: Upload only other apks 2025-12-25 14:49:49 +08:00
世界
84b277615c Fix bugs and add UoT option for naiveproxy outbound 2025-12-25 14:49:49 +08:00
世界
13e425d5c3 Add naiveproxy outbound 2025-12-25 14:49:49 +08:00
世界
626ef0b427 Apply ping destination filter for Windows 2025-12-25 14:49:48 +08:00
世界
277c643c3e 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.
2025-12-25 14:49:48 +08:00
世界
7a4c70ede9 daemon: Add clear logs 2025-12-25 14:49:48 +08:00
世界
55df080e2a Revert "Stop using DHCP on iOS and tvOS" 2025-12-25 14:49:48 +08:00
世界
71253f800e platform: Refactoring libbox to use gRPC-based protocol 2025-12-25 14:49:48 +08:00
世界
30ef92ec7b Add Windows WI-FI state support 2025-12-25 14:49:47 +08:00
世界
5ce866cc8a 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)
2025-12-25 14:49:47 +08:00
世界
eca6a5da18 Add more tcp keep alive options
Also update default TCP keep-alive initial period from 10 minutes to 5 minutes.
2025-12-25 14:49:47 +08:00
世界
37a43dd63a Update quic-go to v0.57.1 2025-12-25 14:49:47 +08:00
世界
2f377b2cdf Fix read credentials for ccm service 2025-12-25 14:49:47 +08:00
世界
fac4068214 Add claude code multiplexer service 2025-12-25 14:49:46 +08:00
世界
ee07065f7b Fix compatibility with MPTCP 2025-12-25 14:49:45 +08:00
世界
f70867e0a9 Use a more conservative strategy for resolving with systemd-resolved for local DNS server 2025-12-25 14:49:45 +08:00
世界
bf43a6655e Fix missing mTLS support in client options 2025-12-25 14:49:45 +08:00
世界
bf055b8ae2 Add curve preferences, pinned public key SHA256 and mTLS for TLS options 2025-12-25 14:49:45 +08:00
世界
f88d249f03 Fix WireGuard input packet 2025-12-25 14:49:45 +08:00
世界
10d6d22b73 Update tfo-go to latest 2025-12-25 14:49:45 +08:00
世界
65e7649952 Remove compatibility codes 2025-12-25 14:49:45 +08:00
世界
d01534aa5c Do not use linkname by default to simplify debugging 2025-12-25 14:49:44 +08:00
世界
3efe0fdfdc documentation: Update chinese translations 2025-12-25 14:49:44 +08:00
世界
67a0c19b07 Update quic-go to v0.55.0 2025-12-25 14:49:44 +08:00
世界
3546a9368b Update WireGuard and Tailscale 2025-12-25 14:49:44 +08:00
世界
8ab5c7695f Fix preConnectionCopy 2025-12-25 14:49:44 +08:00
世界
07190d8d8a Fix ping domain 2025-12-25 14:49:24 +08:00
世界
8e627088c6 release: Fix linux build 2025-12-25 14:49:24 +08:00
世界
f306f704bc Improve ktls rx error handling 2025-12-25 14:49:24 +08:00
世界
537ca35cfe Improve compatibility for kTLS 2025-12-25 14:49:24 +08:00
世界
84a0f240f9 ktls: Add warning for inappropriate scenarios 2025-12-25 14:49:24 +08:00
世界
7f13a66e12 Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2025-12-25 14:49:23 +08:00
世界
301e829266 Add proxy support for ICMP echo request 2025-12-25 14:49:23 +08:00
世界
b04310f285 Fix resolve using resolved 2025-12-25 14:48:56 +08:00
世界
8acef05e95 documentation: Update behavior of local DNS server on darwin 2025-12-25 14:48:56 +08:00
世界
6922ec1070 Remove use of ldflags -checklinkname=0 on darwin 2025-12-25 14:48:56 +08:00
世界
9964bc39da Fix legacy DNS config 2025-12-25 14:48:56 +08:00
世界
644cd773c7 Fix rule-set format 2025-12-25 14:48:56 +08:00
世界
032e00f38d documentation: Remove outdated icons 2025-12-25 14:48:56 +08:00
世界
f9a9845901 documentation: Improve local DNS server 2025-12-25 14:48:52 +08:00
世界
b1fae028ce 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.
2025-12-25 14:48:52 +08:00
世界
07d9ec4f68 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`.
2025-12-25 14:48:52 +08:00
世界
61cecf0b01 Use resolved in local DNS server if available 2025-12-25 14:48:52 +08:00
xchacha20-poly1305
6aa6ee2572 Fix rule set version 2025-12-25 14:48:52 +08:00
世界
591665a302 documentation: Add preferred_by route rule item 2025-12-25 14:48:52 +08:00
世界
1c2d38fcab Add preferred_by route rule item 2025-12-25 14:48:52 +08:00
世界
1357294a63 documentation: Add interface address rule items 2025-12-25 14:48:52 +08:00
世界
a5135e33fd Add interface address rule items 2025-12-25 14:48:51 +08:00
世界
0f772f7bbe Fix ECH retry support 2025-12-25 14:48:51 +08:00
neletor
65f5f406b3 Add support for ech retry configs 2025-12-25 14:48:51 +08:00
Zephyruso
96f1f9e205 Add /dns/flush-clash meta api 2025-12-25 14:48:51 +08:00
世界
f56d9ab945 Bump version 2025-12-25 14:47:10 +08:00
世界
86fabd6a22 Update Mozilla certificates 2025-12-25 14:42:18 +08:00
世界
24a1e7cee4 Ignore darwin IP_DONTFRAG error when not supported 2025-12-25 14:40:48 +08:00
世界
223dd8bb1a Fix TCP DNS response buffer 2025-12-22 13:51:00 +08:00
世界
68448de7d0 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2025-12-22 13:50:57 +08:00
世界
1ebff74c21 Fix DNS cache not working when domain strategy is set
The cache lookup was performed before rule matching, using the caller's
strategy (usually AsIS/0) instead of the resolved strategy. This caused
cache misses when ipv4_only was configured globally but the cache lookup
expected both A and AAAA records.

Remove LookupCache and ExchangeCache from Router, as the cache checks
inside client.Lookup and client.Exchange already handle caching correctly
after rule matching with the proper strategy and transport.
2025-12-21 16:59:10 +08:00
世界
f0cd3422c1 Bump version 2025-12-14 00:09:19 +08:00
世界
e385a98ced Update Go to 1.25.5 2025-12-13 20:11:29 +08:00
世界
670f32baee Fix naive inbound 2025-12-12 21:19:28 +08:00
世界
2747a00ba2 Fix tailscale destination 2025-12-01 15:02:04 +08:00
世界
48e76038d0 Update Go to 1.25.4 2025-11-16 09:53:10 +08:00
世界
6421252d44 release: Fix windows7 build 2025-11-16 09:09:34 +08:00
世界
216c4c8bd4 Fix adapter handler 2025-11-16 08:34:46 +08:00
世界
5841d410a1 ssm-api: Fix save cache 2025-11-04 11:00:43 +08:00
Kumiko as a Service
63c8207d7a Use --no-cache --upgrade option in apk add
No need for separate upgrade / cache cleanup steps.

Signed-off-by: Kumiko as a Service <Dreista@users.noreply.github.com>
2025-11-04 11:00:41 +08:00
世界
54ed58499d Bump version 2025-10-27 18:04:24 +08:00
世界
b1bdc18c85 Fix socks response 2025-10-27 18:03:05 +08:00
世界
a38030cc0b Fix memory leak in hysteria2 2025-10-24 10:52:08 +08:00
世界
4626aa2cb0 Bump version 2025-10-21 21:39:28 +08:00
世界
5a40b673a4 Update dependencies 2025-10-21 21:39:28 +08:00
世界
541f63fee4 redirect: Fix compatibility with /product/bin/su 2025-10-21 21:27:15 +08:00
世界
5de6f4a14f Fix tailscale not enforcing NoLogsNoSupport 2025-10-16 22:30:08 +08:00
世界
5658830077 Fix trailing dot handling in local DNS transport 2025-10-16 21:43:12 +08:00
世界
0e50edc009 documentation: Add appreciate for Warp 2025-10-16 21:43:12 +08:00
世界
444f454810 Bump version 2025-10-14 23:43:36 +08:00
世界
d0e1fd6c7e Update Go to 1.25.3 2025-10-14 23:43:36 +08:00
世界
17b4d1e010 Update uTLS to v1.8.1 2025-10-14 23:40:19 +08:00
世界
06791470c9 Fix DNS reject panic 2025-10-14 23:40:19 +08:00
世界
ef14c8ca0e Disable TCP slow open for anytls
Fixes #3459
2025-10-14 23:40:19 +08:00
世界
36dc883c7c Fix DNS negative caching to comply with RFC 2308 2025-10-09 23:45:23 +08:00
Mahdi
6557bd7029 Fix dns cache in lookup 2025-10-09 23:45:23 +08:00
世界
41b30c91d9 Improve HTTPS DNS transport 2025-10-09 23:45:23 +08:00
194 changed files with 13137 additions and 2629 deletions

1
.github/CRONET_GO_VERSION vendored Normal file
View File

@@ -0,0 +1 @@
1cc61ad20399081362ccbc18d650432d1a6d42ec

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.25.1" VERSION="1.25.5"
mkdir -p $HOME/go mkdir -p $HOME/go
cd $HOME/go cd $HOME/go

13
.github/update_cronet.sh vendored Executable file
View File

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

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -69,13 +69,25 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" } - { os: linux, arch: amd64, variant: purego, naive: true, openwrt: "x86_64" }
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" } - { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: arm64, variant: glibc, naive: true }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: "386", go386: sse2, openwrt: "i386_pentium4" }
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: arm, goarm: "7", openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { 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: "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: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
- { os: linux, arch: arm, 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: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" } - { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" } - { 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: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
@@ -87,35 +99,28 @@ jobs:
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" } - { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 } - { os: android, arch: arm64, ndk: "aarch64-linux-android23" }
- { os: darwin, arch: arm64 } - { os: android, arch: arm, ndk: "armv7a-linux-androideabi23" }
- { os: darwin, arch: amd64, legacy_go124: true, legacy_name: "macos-11" } - { os: android, arch: amd64, ndk: "x86_64-linux-android23" }
- { os: android, arch: "386", ndk: "i686-linux-android23" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
- { os: android, arch: "386", ndk: "i686-linux-android21" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }} if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Go 1.24 - name: Setup Go 1.24
if: matrix.legacy_go124 if: matrix.legacy_go124
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.24.6 go-version: ~1.24.10
- name: Cache Go for Windows 7 - name: Cache Go for Windows 7
if: matrix.legacy_win7 if: matrix.legacy_win7
id: cache-go-for-windows7 id: cache-go-for-windows7
@@ -123,7 +128,7 @@ jobs:
with: with:
path: | path: |
~/go/go_win7 ~/go/go_win7
key: go_win7_1251 key: go_win7_1255
- name: Setup Go for Windows 7 - name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |- run: |-
@@ -139,6 +144,45 @@ jobs:
with: with:
ndk-version: r28 ndk-version: r28
local-cache: true local-cache: true
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go 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: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/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
run: |
set -xeuo pipefail
cd ~/cronet-go
if [[ "${{ matrix.variant }}" == "musl" ]]; then
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
else
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} download-toolchain
fi
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
if [[ "${{ matrix.variant }}" == "musl" ]]; then
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
else
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} env >> $GITHUB_ENV
fi
- name: Set tag - name: Set tag
run: |- run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -146,10 +190,70 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0' 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="${TAGS},with_naive_outbound"
fi
if [[ "${{ matrix.variant }}" == "purego" ]]; then
TAGS="${TAGS},with_purego"
elif [[ "${{ matrix.variant }}" == "musl" ]]; then
TAGS="${TAGS},with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build (purego)
if: matrix.os != 'android' if: matrix.variant == 'purego'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract libcronet.so
if: matrix.variant == 'purego' && matrix.naive
run: |
cd ~/cronet-go
CGO_ENABLED=0 go run -v ./cmd/build-naive extract-lib --target ${{ matrix.os }}/${{ matrix.arch }} -o $GITHUB_WORKSPACE/dist
- name: Build (glibc)
if: matrix.variant == 'glibc'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (musl)
if: matrix.variant == 'musl'
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-variant)
if: matrix.os != 'android' && matrix.variant == ''
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
@@ -193,6 +297,11 @@ jobs:
elif [[ -n "${{ matrix.legacy_name }}" ]]; then elif [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}" DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi fi
if [[ "${{ matrix.variant }}" == "glibc" ]]; then
DIR_NAME="${DIR_NAME}-glibc"
elif [[ "${{ matrix.variant }}" == "musl" ]]; then
DIR_NAME="${DIR_NAME}-musl"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}" PKG_VERSION="${PKG_VERSION//-/\~}"
@@ -260,8 +369,12 @@ jobs:
-p "dist/openwrt.deb" \ -p "dist/openwrt.deb" \
--architecture all \ --architecture all \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
SUFFIX=""
if [[ "${{ matrix.variant }}" == "musl" ]]; then
SUFFIX="_musl"
fi
for architecture in ${{ matrix.openwrt }}; do for architecture in ${{ matrix.openwrt }}; do
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk" .github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}${SUFFIX}.ipk"
done done
rm "dist/openwrt.deb" rm "dist/openwrt.deb"
- name: Archive - name: Archive
@@ -275,15 +388,177 @@ jobs:
zip -r "${DIR_NAME}.zip" "${DIR_NAME}" zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else else
cp sing-box "${DIR_NAME}" cp sing-box "${DIR_NAME}"
if [ -f libcronet.so ]; then
cp libcronet.so "${DIR_NAME}"
fi
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}" tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
fi fi
rm -r "${DIR_NAME}" rm -r "${DIR_NAME}"
- name: Cleanup
run: rm -f dist/sing-box dist/libcronet.so
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}${{ matrix.variant && format('-{0}', matrix.variant) }}
path: "dist"
build_darwin:
name: Build Darwin binaries
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
runs-on: macos-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { arch: amd64 }
- { arch: arm64 }
- { 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_go124 }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
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"
git tag v${{ needs.calculate_version.outputs.version }} -f
- 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.legacy_go124 }}" != "true" ]]; then
TAGS="${TAGS},with_naive_outbound"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
run: |
set -xeuo pipefail
mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: darwin
GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name
run: |-
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-darwin-${{ matrix.arch }}"
if [[ -n "${{ matrix.legacy_name }}" ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
- name: Archive
run: |
set -xeuo pipefail
cd dist
mkdir -p "${DIR_NAME}"
cp ../LICENSE "${DIR_NAME}"
cp sing-box "${DIR_NAME}"
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
rm -r "${DIR_NAME}"
- name: Cleanup - name: Cleanup
run: rm dist/sing-box run: rm dist/sing-box
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }} name: binary-darwin_${{ matrix.arch }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }}
path: "dist"
build_windows:
name: Build Windows binaries
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
runs-on: windows-latest
needs:
- calculate_version
strategy:
matrix:
include:
- { arch: amd64, naive: true }
- { arch: "386" }
- { arch: arm64, naive: true }
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.4
- name: Set tag
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$env:GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: Build
if: matrix.naive
run: |
mkdir -p dist
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 }} -checklinkname=0" `
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: windows
GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
if: ${{ !matrix.naive }}
run: |
mkdir -p dist
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 }} -checklinkname=0" `
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: windows
GOARCH: ${{ matrix.arch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract libcronet.dll
if: matrix.naive
run: |
$CRONET_GO_VERSION = Get-Content .github/CRONET_GO_VERSION
$env:CGO_ENABLED = "0"
go run -v "github.com/sagernet/cronet-go/cmd/build-naive@$CRONET_GO_VERSION" extract-lib --target windows/${{ matrix.arch }} -o dist
- name: Archive
if: matrix.naive
run: |
$DIR_NAME = "sing-box-${{ needs.calculate_version.outputs.version }}-windows-${{ matrix.arch }}"
mkdir "dist/$DIR_NAME"
Copy-Item LICENSE "dist/$DIR_NAME"
Copy-Item "dist/sing-box.exe" "dist/$DIR_NAME"
Copy-Item "dist/libcronet.dll" "dist/$DIR_NAME"
Compress-Archive -Path "dist/$DIR_NAME" -DestinationPath "dist/$DIR_NAME.zip"
Remove-Item -Recurse "dist/$DIR_NAME"
- name: Archive
if: ${{ !matrix.naive }}
run: |
$DIR_NAME = "sing-box-${{ needs.calculate_version.outputs.version }}-windows-${{ matrix.arch }}"
mkdir "dist/$DIR_NAME"
Copy-Item LICENSE "dist/$DIR_NAME"
Copy-Item "dist/sing-box.exe" "dist/$DIR_NAME"
Compress-Archive -Path "dist/$DIR_NAME" -DestinationPath "dist/$DIR_NAME.zip"
Remove-Item -Recurse "dist/$DIR_NAME"
- name: Cleanup
if: matrix.naive
run: Remove-Item dist/sing-box.exe, dist/libcronet.dll
- name: Cleanup
if: ${{ !matrix.naive }}
run: Remove-Item dist/sing-box.exe
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-windows_${{ matrix.arch }}
path: "dist" path: "dist"
build_android: build_android:
name: Build Android name: Build Android
@@ -300,7 +575,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -348,9 +623,9 @@ jobs:
- name: Build - name: Build
run: |- run: |-
mkdir clients/android/app/libs mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs cp *.aar clients/android/app/libs
cd clients/android cd clients/android
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease ./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease
env: env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -358,8 +633,18 @@ jobs:
- name: Prepare upload - name: Prepare upload
run: |- run: |-
mkdir -p dist mkdir -p dist
cp clients/android/app/build/outputs/apk/play/release/*.apk dist #cp clients/android/app/build/outputs/apk/play/release/*.apk dist
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist cp clients/android/app/build/outputs/apk/other/release/*.apk dist
cp clients/android/app/build/outputs/apk/otherLegacy/release/*.apk dist
VERSION_CODE=$(grep VERSION_CODE clients/android/version.properties | cut -d= -f2)
VERSION_NAME=$(grep VERSION_NAME clients/android/version.properties | cut -d= -f2)
cat > dist/SFA-version-metadata.json << EOF
{
"version_code": ${VERSION_CODE},
"version_name": "${VERSION_NAME}"
}
EOF
cat dist/SFA-version-metadata.json
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -380,7 +665,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -421,7 +706,7 @@ jobs:
run: |- run: |-
go run -v ./cmd/internal/update_android_version --ci go run -v ./cmd/internal/update_android_version --ci
mkdir clients/android/app/libs mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs cp *.aar clients/android/app/libs
cd clients/android cd clients/android
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
./gradlew :app:publishPlayReleaseBundle ./gradlew :app:publishPlayReleaseBundle
@@ -433,7 +718,7 @@ jobs:
build_apple: build_apple:
name: Build Apple clients name: Build Apple clients
runs-on: macos-26 runs-on: macos-26
if: false if: false # github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone'
needs: needs:
- calculate_version - calculate_version
strategy: strategy:
@@ -479,7 +764,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-
@@ -598,7 +883,7 @@ jobs:
--app-drop-link 0 0 \ --app-drop-link 0 0 \
--skip-jenkins \ --skip-jenkins \
SFM.dmg "${{ matrix.export_path }}/SFM.app" SFM.dmg "${{ matrix.export_path }}/SFM.app"
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password" xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
cd "${{ matrix.archive }}" cd "${{ matrix.archive }}"
zip -r SFM.dSYMs.zip dSYMs zip -r SFM.dSYMs.zip dSYMs
popd popd
@@ -619,6 +904,8 @@ jobs:
needs: needs:
- calculate_version - calculate_version
- build - build
- build_darwin
- build_windows
- build_android - build_android
- build_apple - build_apple
steps: steps:

View File

@@ -1,6 +1,10 @@
name: Publish Docker Images name: Publish Docker Images
on: on:
#push:
# branches:
# - main-next
# - dev-next
release: release:
types: types:
- published - published
@@ -13,8 +17,134 @@ env:
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
jobs: jobs:
build: build_binary:
name: Build binary
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
include:
# Naive-enabled builds (musl)
- { arch: amd64, naive: true, docker_platform: "linux/amd64" }
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
- { arch: "386", naive: true, docker_platform: "linux/386" }
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
# 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
id: ref
run: |-
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
ref="${{ github.ref_name }}"
else
ref="${{ github.event.inputs.tag }}"
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.4
- name: Clone cronet-go
if: matrix.naive
run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go 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: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with:
path: |
~/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
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set version
run: |
set -xeuo pipefail
VERSION=$(go run ./cmd/internal/read_tag)
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
- name: Set build tags
run: |
set -xeuo pipefail
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="${TAGS},with_naive_outbound,with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${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}\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
- 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}\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box
env:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
- name: Prepare artifact
run: |
platform=${{ matrix.docker_platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
# Rename binary to include arch info for Dockerfile.binary
BINARY_NAME="sing-box-${{ matrix.arch }}"
if [[ -n "${{ matrix.goarm }}" ]]; then
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
fi
mv sing-box "${BINARY_NAME}"
echo "BINARY_NAME=${BINARY_NAME}" >> $GITHUB_ENV
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: ${{ env.BINARY_NAME }}
if-no-files-found: error
retention-days: 1
build_docker:
name: Build Docker image
runs-on: ubuntu-latest
needs:
- build_binary
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -47,6 +177,16 @@ jobs:
run: | run: |
platform=${{ matrix.platform }} platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Download binary
uses: actions/download-artifact@v5
with:
name: binary-${{ env.PLATFORM_PAIR }}
path: .
- name: Prepare binary
run: |
# Find and make the binary executable
chmod +x sing-box-*
ls -la sing-box-*
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx - name: Setup Docker Buildx
@@ -68,8 +208,7 @@ jobs:
with: with:
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
context: . context: .
build-args: | file: Dockerfile.binary
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest - name: Export digest
@@ -87,7 +226,7 @@ jobs:
merge: merge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build - build_docker
steps: steps:
- name: Get commit to build - name: Get commit to build
id: ref id: ref
@@ -121,6 +260,7 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push - name: Create manifest list and push
if: github.event_name != 'push'
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
@@ -128,6 +268,7 @@ jobs:
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \ -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image - name: Inspect image
if: github.event_name != 'push'
run: | run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -1,6 +1,10 @@
name: Build Linux Packages name: Build Linux Packages
on: on:
#push:
# branches:
# - main-next
# - dev-next
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
@@ -30,7 +34,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -52,11 +56,13 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 } # Naive-enabled builds (musl)
- { os: linux, arch: "386", debian: i386, rpm: i386 } - { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
# Non-naive builds (unsupported architectures)
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl } - { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el } - { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel } - { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: s390x, debian: s390x, rpm: s390x }
@@ -71,13 +77,38 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Android NDK - name: Clone cronet-go
if: matrix.os == 'android' if: matrix.naive
uses: nttld/setup-ndk@v1 run: |
set -xeuo pipefail
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
git init ~/cronet-go
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
git -C ~/cronet-go 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: Cache Chromium toolchain
if: matrix.naive
id: cache-chromium-toolchain
uses: actions/cache@v4
with: with:
ndk-version: r28 path: |
local-cache: true ~/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
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
- name: Set Chromium toolchain environment
if: matrix.naive
run: |
set -xeuo pipefail
cd ~/cronet-go
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
- name: Set tag - name: Set tag
run: |- run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -85,9 +116,27 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0' 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="${TAGS},with_naive_outbound,with_musl"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - 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 '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
./cmd/sing-box
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: ${{ matrix.arch }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-naive)
if: ${{ ! matrix.naive }}
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
@@ -185,5 +234,6 @@ jobs:
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Publish packages - name: Publish packages
if: github.event_name != 'push'
run: |- run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/ ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/

View File

@@ -1,103 +0,0 @@
project_name: sing-box
builds:
- id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
mod_timestamp: '{{ .CommitTimestamp }}'
snapshot:
name_template: "{{ .Version }}.{{ .ShortCommit }}"
nfpms:
- &template
id: package
package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
conflicts:
- sing-box-beta
- id: package_beta
<<: *template
package_name: sing-box-beta
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
formats:
- deb
- rpm
conflicts:
- sing-box
release:
disable: true
furies:
- account: sagernet
ids:
- package
disable: "{{ not (not .Prerelease) }}"
- account: sagernet
ids:
- package_beta
disable: "{{ not .Prerelease }}"

View File

@@ -1,213 +0,0 @@
version: 2
project_name: sing-box
builds:
- &template
id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: legacy
<<: *template
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go_legacy
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
targets:
- windows_amd64_v1
- windows_386
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
goarm: 7
env:
- CC=armv7a-linux-androideabi21-clang
- CXX=armv7a-linux-androideabi21-clang++
- goos: android
goarch: arm64
env:
- CC=aarch64-linux-android21-clang
- CXX=aarch64-linux-android21-clang++
- goos: android
goarch: 386
env:
- CC=i686-linux-android21-clang
- CXX=i686-linux-android21-clang++
- goos: android
goarch: amd64
goamd64: v1
env:
- CC=x86_64-linux-android21-clang
- CXX=x86_64-linux-android21-clang++
targets:
- android_arm_7
- android_arm64
- android_386
- android_amd64
archives:
- &template
id: archive
builds:
- main
- android
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy
<<: *template
builds:
- legacy
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
nfpms:
- id: package
package_name: sing-box
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
builds:
- main
homepage: https://sing-box.sagernet.org/
maintainer: nekohasekai <contact-git@sekai.icu>
description: The universal proxy platform.
license: GPLv3 or later
formats:
- deb
- rpm
- archlinux
# - apk
# - ipk
priority: extra
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
deb:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
fields:
Bugs: https://github.com/SagerNet/sing-box/issues
rpm:
signature:
key_file: "{{ .Env.NFPM_KEY_PATH }}"
overrides:
apk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/sing-box.initd
dst: /etc/init.d/sing-box
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash
- src: release/completions/sing-box.fish
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
- src: release/completions/sing-box.zsh
dst: /usr/share/zsh/site-functions/_sing-box
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
ipk:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
- src: release/config/openwrt.init
dst: /etc/init.d/sing-box
- src: release/config/openwrt.conf
dst: /etc/config/sing-box
source:
enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
checksum:
disable: true
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
signs:
- artifacts: checksum
release:
github:
owner: SagerNet
name: sing-box
draft: true
prerelease: auto
mode: replace
ids:
- archive
- package
skip_upload: true
partial:
by: target

View File

@@ -13,15 +13,13 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \ && go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0" \ "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 \ -o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
./cmd/sing-box ./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \ RUN set -ex \
&& apk upgrade \ && apk add --no-cache --upgrade bash tzdata ca-certificates nftables
&& apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"] ENTRYPOINT ["sing-box"]

8
Dockerfile.binary Normal file
View File

@@ -0,0 +1,8 @@
FROM alpine
ARG TARGETARCH
ARG TARGETVARIANT
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& 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,6 +1,6 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0 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) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
@@ -37,6 +37,9 @@ fmt:
@gofmt -s -w . @gofmt -s -w .
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_docs:
go run ./cmd/internal/format_docs
fmt_install: fmt_install:
go install -v mvdan.cc/gofumpt@v0.8.0 go install -v mvdan.cc/gofumpt@v0.8.0
go install -v github.com/daixiang0/gci@latest go install -v github.com/daixiang0/gci@latest
@@ -249,8 +252,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.10
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.10
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve

View File

@@ -1,3 +1,11 @@
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
<a href="https://go.warp.dev/sing-box">
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
</a>
---
# sing-box # sing-box
The universal proxy platform. The universal proxy platform.

View File

@@ -27,8 +27,6 @@ type DNSClient interface {
Start() Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache() ClearCache()
} }
@@ -70,6 +68,7 @@ type DNSTransport interface {
Type() string Type() string
Tag() string Tag() string
Dependencies() []string Dependencies() []string
Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -11,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
var _ adapter.EndpointManager = (*Manager)(nil) var _ adapter.EndpointManager = (*Manager)(nil)
@@ -46,10 +48,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
return nil return nil
} }
for _, endpoint := range m.endpoints { for _, endpoint := range m.endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(endpoint, stage) err := adapter.LegacyStart(endpoint, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -66,11 +72,15 @@ func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout) monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error var err error
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, endpoint.Close(), func(err error) error { err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -119,11 +129,15 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if m.started { if m.started {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(endpoint, stage) err = adapter.LegacyStart(endpoint, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {

View File

@@ -6,6 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"time" "time"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
) )
@@ -14,6 +15,7 @@ type ClashServer interface {
ConnectionTracker ConnectionTracker
Mode() string Mode() string
ModeList() []string ModeList() []string
SetModeUpdateHook(hook *observable.Subscriber[struct{}])
HistoryStorage() URLTestHistoryStorage HistoryStorage() URLTestHistoryStorage
} }
@@ -23,7 +25,7 @@ type URLTestHistory struct {
} }
type URLTestHistoryStorage interface { type URLTestHistoryStorage interface {
SetHook(hook chan<- struct{}) SetHook(hook *observable.Subscriber[struct{}])
LoadURLTestHistory(tag string) *URLTestHistory LoadURLTestHistory(tag string) *URLTestHistory
DeleteURLTestHistory(tag string) DeleteURLTestHistory(tag string)
StoreURLTestHistory(tag string, history *URLTestHistory) StoreURLTestHistory(tag string, history *URLTestHistory)

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -11,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
var _ adapter.InboundManager = (*Manager)(nil) var _ adapter.InboundManager = (*Manager)(nil)
@@ -45,10 +47,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
inbounds := m.inbounds inbounds := m.inbounds
m.access.Unlock() m.access.Unlock()
for _, inbound := range inbounds { for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(inbound, stage) err := adapter.LegacyStart(inbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -65,11 +71,15 @@ func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout) monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error var err error
for _, inbound := range inbounds { for _, inbound := range inbounds {
monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]") name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, inbound.Close(), func(err error) error { err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]") return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -121,11 +131,15 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if m.started { if m.started {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(inbound, stage) err = adapter.LegacyStart(inbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if existsInbound, loaded := m.inboundByTag[tag]; loaded { if existsInbound, loaded := m.inboundByTag[tag]; loaded {

View File

@@ -1,6 +1,14 @@
package adapter package adapter
import E "github.com/sagernet/sing/common/exceptions" import (
"reflect"
"strings"
"time"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
type SimpleLifecycle interface { type SimpleLifecycle interface {
Start() error Start() error
@@ -48,22 +56,47 @@ type LifecycleService interface {
Lifecycle Lifecycle
} }
func Start(stage StartStage, services ...Lifecycle) error { func getServiceName(service any) string {
if named, ok := service.(interface {
Type() string
Tag() string
}); ok {
tag := named.Tag()
if tag != "" {
return named.Type() + "[" + tag + "]"
}
return named.Type()
}
t := reflect.TypeOf(service)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return strings.ToLower(t.Name())
}
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
for _, service := range services { for _, service := range services {
name := getServiceName(service)
logger.Trace(stage, " ", name)
startTime := time.Now()
err := service.Start(stage) err := service.Start(stage)
if err != nil { if err != nil {
return err return err
} }
logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
func StartNamed(stage StartStage, services []LifecycleService) error { func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
for _, service := range services { for _, service := range services {
logger.Trace(stage, " ", service.Name())
startTime := time.Now()
err := service.Start(stage) err := service.Start(stage)
if err != nil { if err != nil {
return E.Cause(err, stage.String(), " ", service.Name()) return E.Cause(err, stage.String(), " ", service.Name())
} }
logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -13,6 +14,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
@@ -81,10 +83,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
outbounds := m.outbounds outbounds := m.outbounds
m.access.Unlock() m.access.Unlock()
for _, outbound := range outbounds { for _, outbound := range outbounds {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(outbound, stage) err := adapter.LegacyStart(outbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
return nil return nil
@@ -109,22 +115,29 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
} }
started[outboundTag] = true started[outboundTag] = true
canContinue = true canContinue = true
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter { if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]") m.logger.Trace("start ", name)
startTime := time.Now()
monitor.Start("start ", name)
err := starter.Start(adapter.StartStateStart) err := starter.Start(adapter.StartStateStart)
monitor.Finish() monitor.Finish()
if err != nil { if err != nil {
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]") return E.Cause(err, "start ", name)
} }
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} else if starter, isStarter := outboundToStart.(interface { } else if starter, isStarter := outboundToStart.(interface {
Start() error Start() error
}); isStarter { }); isStarter {
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]") m.logger.Trace("start ", name)
startTime := time.Now()
monitor.Start("start ", name)
err := starter.Start() err := starter.Start()
monitor.Finish() monitor.Finish()
if err != nil { if err != nil {
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]") return E.Cause(err, "start ", name)
} }
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if len(started) == len(outbounds) { if len(started) == len(outbounds) {
@@ -171,11 +184,15 @@ func (m *Manager) Close() error {
var err error var err error
for _, outbound := range outbounds { for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser { if closer, isCloser := outbound.(io.Closer); isCloser {
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]") name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, closer.Close(), func(err error) error { err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]") return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
return nil return nil
@@ -256,11 +273,15 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
return err return err
} }
if m.started { if m.started {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(outbound, stage) err = adapter.LegacyStart(outbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
m.access.Lock() m.access.Lock()

View File

@@ -21,7 +21,7 @@ import (
type Router interface { type Router interface {
Lifecycle Lifecycle
ConnectionRouter ConnectionRouter
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error)
ConnectionRouterEx ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool) RuleSet(tag string) (RuleSet, bool)
Rules() []Rule Rules() []Rule

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -11,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
var _ adapter.ServiceManager = (*Manager)(nil) var _ adapter.ServiceManager = (*Manager)(nil)
@@ -43,10 +45,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
services := m.services services := m.services
m.access.Unlock() m.access.Unlock()
for _, service := range services { for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(service, stage) err := adapter.LegacyStart(service, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -63,11 +69,15 @@ func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout) monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error var err error
for _, service := range services { for _, service := range services {
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]") name := "service/" + service.Type() + "[" + service.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, service.Close(), func(err error) error { err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]") return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -116,11 +126,15 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if m.started { if m.started {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(service, stage) err = adapter.LegacyStart(service, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]") return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if existsService, loaded := m.serviceByTag[tag]; loaded { if existsService, loaded := m.serviceByTag[tag]; loaded {

View File

@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
} }
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx) _, myMetadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
myMetadata.Source = source myMetadata.Source = source
} }
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
} }
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx) _, myMetadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
myMetadata.Source = source myMetadata.Source = source
} }
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
} }
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx) _, metadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
metadata.Source = source metadata.Source = source
} }
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
} }
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx) _, metadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
metadata.Source = source metadata.Source = source
} }

49
box.go
View File

@@ -443,15 +443,15 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return E.Cause(err, "start logger") return E.Cause(err, "start logger")
} }
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router) err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil { if err != nil {
return err return err
} }
@@ -463,27 +463,27 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStateStart, s.internalService) err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService) err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService) err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
if err != nil { if err != nil {
return err return err
} }
@@ -497,17 +497,42 @@ func (s *Box) Close() error {
default: default:
close(s.done) close(s.done)
} }
err := common.Close( var err error
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, for _, closeItem := range []struct {
) name string
service adapter.Lifecycle
}{
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
{"dns-router", s.dnsRouter},
{"dns-transport", s.dnsTransport},
{"network", s.network},
} {
s.logger.Trace("close ", closeItem.name)
startTime := time.Now()
err = E.Append(err, closeItem.service.Close(), func(err error) error {
return E.Cause(err, "close ", closeItem.name)
})
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
for _, lifecycleService := range s.internalService { for _, lifecycleService := range s.internalService {
s.logger.Trace("close ", lifecycleService.Name())
startTime := time.Now()
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) return E.Cause(err, "close ", lifecycleService.Name())
}) })
s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
s.logger.Trace("close logger")
startTime := time.Now()
err = E.Append(err, s.logFactory.Close(), func(err error) error { err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger") return E.Cause(err, "close logger")
}) })
s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
return err return err
} }

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
_ "github.com/sagernet/gomobile" _ "github.com/sagernet/gomobile"
@@ -46,7 +47,7 @@ var (
sharedFlags []string sharedFlags []string
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
macOSTags []string darwinTags []string
memcTags []string memcTags []string
notMemcTags []string notMemcTags []string
debugTags []string debugTags []string
@@ -62,16 +63,34 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0") sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -checklinkname=0") debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
macOSTags = append(macOSTags, "with_dhcp") darwinTags = append(darwinTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale") memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory") notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }
func buildAndroid() { type AndroidBuildConfig struct {
build_shared.FindSDK() AndroidAPI int
OutputName string
Tags []string
}
func filterTags(tags []string, exclude ...string) []string {
excludeMap := make(map[string]bool)
for _, tag := range exclude {
excludeMap[tag] = true
}
var result []string
for _, tag := range tags {
if !excludeMap[tag] {
result = append(result, tag)
}
}
return result
}
func checkJavaVersion() {
var javaPath string var javaPath string
javaHome := os.Getenv("JAVA_HOME") javaHome := os.Getenv("JAVA_HOME")
if javaHome == "" { if javaHome == "" {
@@ -87,21 +106,24 @@ func buildAndroid() {
if !strings.Contains(javaVersion, "openjdk 17") { if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17") log.Fatal("java version should be openjdk 17")
} }
}
var bindTarget string func getAndroidBindTarget() string {
if platform != "" { if platform != "" {
bindTarget = platform return platform
} else if debugEnabled { } else if debugEnabled {
bindTarget = "android/arm64" return "android/arm64"
} else {
bindTarget = "android"
} }
return "android"
}
func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-o", config.OutputName,
"-target", bindTarget, "-target", bindTarget,
"-androidapi", "21", "-androidapi", strconv.Itoa(config.AndroidAPI),
"-javapkg=io.nekohasekai", "-javapkg=io.nekohasekai",
"-libname=box", "-libname=box",
} }
@@ -112,34 +134,59 @@ func buildAndroid() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := append(sharedTags, memcTags...) args = append(args, "-tags", strings.Join(config.Tags, ","))
if debugEnabled {
tags = append(tags, debugTags...)
}
args = append(args, "-tags", strings.Join(tags, ","))
args = append(args, "./experimental/libbox") args = append(args, "./experimental/libbox")
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
command.Stdout = os.Stdout command.Stdout = os.Stdout
command.Stderr = os.Stderr command.Stderr = os.Stderr
err = command.Run() err := command.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.IsDir(copyPath) { if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath) copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name)) err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Info("copied to ", copyPath) log.Info("copied ", config.OutputName, " to ", copyPath)
} }
} }
func buildAndroid() {
build_shared.FindSDK()
checkJavaVersion()
bindTarget := getAndroidBindTarget()
// Build main variant (SDK 23)
mainTags := append([]string{}, sharedTags...)
mainTags = append(mainTags, memcTags...)
if debugEnabled {
mainTags = append(mainTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 23,
OutputName: "libbox.aar",
Tags: mainTags,
}, bindTarget)
// Build legacy variant (SDK 21, no naive outbound)
legacyTags := filterTags(sharedTags, "with_naive_outbound")
legacyTags = append(legacyTags, memcTags...)
if debugEnabled {
legacyTags = append(legacyTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 21,
OutputName: "libbox-legacy.aar",
Tags: legacyTags,
}, bindTarget)
}
func buildApple() { func buildApple() {
var bindTarget string var bindTarget string
if platform != "" { if platform != "" {
@@ -158,9 +205,7 @@ func buildApple() {
"-tags-not-macos=with_low_memory", "-tags-not-macos=with_low_memory",
} }
if !withTailscale { if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ",")) args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
} else {
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
} }
if !debugEnabled { if !debugEnabled {
@@ -169,7 +214,7 @@ func buildApple() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := sharedTags tags := append(sharedTags, darwinTags...)
if withTailscale { if withTailscale {
tags = append(tags, memcTags...) tags = append(tags, memcTags...)
} }

View File

@@ -0,0 +1,117 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/log"
)
func main() {
err := filepath.Walk("docs", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".md") {
return nil
}
return processFile(path)
})
if err != nil {
log.Fatal(err)
}
}
func processFile(path string) error {
content, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(string(content), "\n")
modified := false
result := make([]string, 0, len(lines))
inQuoteBlock := false
materialLines := []int{} // indices of :material- lines in the block
for _, line := range lines {
// Check for quote block start
if strings.HasPrefix(line, "!!! quote \"") && strings.Contains(line, "sing-box") {
inQuoteBlock = true
materialLines = nil
result = append(result, line)
continue
}
// Inside a quote block
if inQuoteBlock {
trimmed := strings.TrimPrefix(line, " ")
isMaterialLine := strings.HasPrefix(trimmed, ":material-")
isEmpty := strings.TrimSpace(line) == ""
isIndented := strings.HasPrefix(line, " ")
if isMaterialLine {
materialLines = append(materialLines, len(result))
result = append(result, line)
continue
}
// Block ends when:
// - Empty line AFTER we've seen material lines, OR
// - Non-indented, non-empty line
blockEnds := (isEmpty && len(materialLines) > 0) || (!isEmpty && !isIndented)
if blockEnds {
// Process collected material lines
if len(materialLines) > 0 {
for j, idx := range materialLines {
isLast := j == len(materialLines)-1
resultLine := strings.TrimRight(result[idx], " ")
if !isLast {
// Add trailing two spaces for non-last lines
resultLine += " "
}
if result[idx] != resultLine {
modified = true
result[idx] = resultLine
}
}
}
inQuoteBlock = false
materialLines = nil
}
}
result = append(result, line)
}
// Handle case where file ends while still in a block
if inQuoteBlock && len(materialLines) > 0 {
for j, idx := range materialLines {
isLast := j == len(materialLines)-1
resultLine := strings.TrimRight(result[idx], " ")
if !isLast {
resultLine += " "
}
if result[idx] != resultLine {
modified = true
result[idx] = resultLine
}
}
}
if modified {
newContent := strings.Join(result, "\n")
if !bytes.Equal(content, []byte(newContent)) {
log.Info("formatted: ", path)
return os.WriteFile(path, []byte(newContent), 0o644)
}
}
return nil
}

View File

@@ -17,6 +17,10 @@ func main() {
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
err = updateChromeIncludedRootCAs()
if err != nil {
log.Error(err)
}
} }
func updateMozillaIncludedRootCAs() error { func updateMozillaIncludedRootCAs() error {
@@ -69,3 +73,94 @@ func init() {
generated.WriteString("}\n") generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644) return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
} }
func fetchChinaFingerprints() (map[string]bool, error) {
response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv4")
if err != nil {
return nil, err
}
defer response.Body.Close()
reader := csv.NewReader(response.Body)
header, err := reader.Read()
if err != nil {
return nil, err
}
countryIndex := slices.Index(header, "Country")
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
chinaFingerprints := make(map[string]bool)
for {
record, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
if record[countryIndex] == "China" {
chinaFingerprints[record[fingerprintIndex]] = true
}
}
return chinaFingerprints, nil
}
func updateChromeIncludedRootCAs() error {
chinaFingerprints, err := fetchChinaFingerprints()
if err != nil {
return err
}
response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/RootCACertificatesIncludedByRSReportCSV")
if err != nil {
return err
}
defer response.Body.Close()
reader := csv.NewReader(response.Body)
header, err := reader.Read()
if err != nil {
return err
}
subjectIndex := slices.Index(header, "Subject")
statusIndex := slices.Index(header, "Google Chrome Status")
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
generated := strings.Builder{}
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import "crypto/x509"
var chromeIncluded *x509.CertPool
func init() {
chromeIncluded = x509.NewCertPool()
`)
for {
record, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return err
}
if record[statusIndex] != "Included" {
continue
}
if chinaFingerprints[record[fingerprintIndex]] {
continue
}
generated.WriteString("\n // ")
generated.WriteString(record[subjectIndex])
generated.WriteString("\n")
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes if present
if len(cert) > 0 && cert[0] == '\'' {
cert = cert[1 : len(cert)-1]
}
generated.WriteString(cert)
generated.WriteString("`))\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
}

2817
common/certificate/chrome.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,8 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
} }
case C.CertificateStoreMozilla: case C.CertificateStoreMozilla:
systemPool = mozillaIncluded systemPool = mozillaIncluded
case C.CertificateStoreChrome:
systemPool = chromeIncluded
case C.CertificateStoreNone: case C.CertificateStoreNone:
systemPool = nil systemPool = nil
default: default:

View File

@@ -142,9 +142,18 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else { } else {
dialer.Timeout = C.TCPConnectTimeout dialer.Timeout = C.TCPConnectTimeout
} }
// TODO: Add an option to customize the keep alive period if !options.DisableTCPKeepAlive {
dialer.KeepAlive = C.TCPKeepAliveInitial keepIdle := time.Duration(options.TCPKeepAlive)
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)) if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial
}
keepInterval := time.Duration(options.TCPKeepAliveInterval)
if keepInterval == 0 {
keepInterval = C.TCPKeepAliveInterval
}
dialer.KeepAlive = keepIdle
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
}
var udpFragment bool var udpFragment bool
if options.UDPFragment != nil { if options.UDPFragment != nil {
udpFragment = *options.UDPFragment udpFragment = *options.UDPFragment

View File

@@ -37,7 +37,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.listenOptions.ReuseAddr { if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
} }
if l.listenOptions.TCPKeepAlive >= 0 { if !l.listenOptions.DisableTCPKeepAlive {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial keepIdle = C.TCPKeepAliveInitial

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings package settings
import ( import (
@@ -134,7 +136,9 @@ func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *db
if !ok { if !ok {
return return
} }
if signal.Name == "PropertyChanged" { // godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.PropertyChanged"),
// not just the member name. This differs from the D-Bus signal member in the match rule.
if signal.Name == "net.connman.Service.PropertyChanged" {
state := m.ReadWIFIState() state := m.ReadWIFIState()
if state != lastState { if state != lastState {
lastState = state lastState = state
@@ -154,6 +158,10 @@ func (m *connmanMonitor) Close() error {
close(m.signalChan) close(m.signalChan)
} }
if m.conn != nil { if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchInterface("net.connman.Service"),
dbus.WithMatchSender("net.connman"),
)
return m.conn.Close() return m.conn.Close()
} }
return nil return nil

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings package settings
import ( import (
@@ -178,6 +180,10 @@ func (m *iwdMonitor) Close() error {
close(m.signalChan) close(m.signalChan)
} }
if m.conn != nil { if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
dbus.WithMatchSender("net.connman.iwd"),
)
return m.conn.Close() return m.conn.Close()
} }
return nil return nil

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings package settings
import ( import (
@@ -40,57 +42,59 @@ func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState {
nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
var primaryConnectionPath dbus.ObjectPath var activeConnectionPaths []dbus.ObjectPath
err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "PrimaryConnection").Store(&primaryConnectionPath) err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "ActiveConnections").Store(&activeConnectionPaths)
if err != nil || primaryConnectionPath == "/" { if err != nil || len(activeConnectionPaths) == 0 {
return adapter.WIFIState{} return adapter.WIFIState{}
} }
connObj := m.conn.Object("org.freedesktop.NetworkManager", primaryConnectionPath) for _, connectionPath := range activeConnectionPaths {
connObj := m.conn.Object("org.freedesktop.NetworkManager", connectionPath)
var devicePaths []dbus.ObjectPath var devicePaths []dbus.ObjectPath
err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths) err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths)
if err != nil || len(devicePaths) == 0 { if err != nil || len(devicePaths) == 0 {
return adapter.WIFIState{}
}
for _, devicePath := range devicePaths {
deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath)
var deviceType uint32
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
if err != nil || deviceType != 2 {
continue continue
} }
var accessPointPath dbus.ObjectPath for _, devicePath := range devicePaths {
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath) deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath)
if err != nil || accessPointPath == "/" {
continue
}
apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath) var deviceType uint32
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
if err != nil || deviceType != 2 {
continue
}
var ssidBytes []byte var accessPointPath dbus.ObjectPath
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes) err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath)
if err != nil { if err != nil || accessPointPath == "/" {
continue continue
} }
var hwAddress string apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath)
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress)
if err != nil {
continue
}
ssid := strings.TrimSpace(string(ssidBytes)) var ssidBytes []byte
if ssid == "" { err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes)
continue if err != nil {
} continue
}
return adapter.WIFIState{ var hwAddress string
SSID: ssid, err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress)
BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")), if err != nil {
continue
}
ssid := strings.TrimSpace(string(ssidBytes))
if ssid == "" {
continue
}
return adapter.WIFIState{
SSID: ssid,
BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")),
}
} }
} }
@@ -151,6 +155,10 @@ func (m *networkManagerMonitor) Close() error {
close(m.signalChan) close(m.signalChan)
} }
if m.conn != nil { if m.conn != nil {
m.conn.RemoveMatchSignal(
dbus.WithMatchSender("org.freedesktop.NetworkManager"),
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
)
return m.conn.Close() return m.conn.Close()
} }
return nil return nil

View File

@@ -8,15 +8,21 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
) )
var wpaSocketCounter atomic.Uint64
type wpaSupplicantMonitor struct { type wpaSupplicantMonitor struct {
socketPath string socketPath string
callback func(adapter.WIFIState) callback func(adapter.WIFIState)
cancel context.CancelFunc cancel context.CancelFunc
monitorConn *net.UnixConn
connMutex sync.Mutex
} }
func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
@@ -31,7 +37,8 @@ func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, err
continue continue
} }
socketPath := filepath.Join(socketDir, entry.Name()) socketPath := filepath.Join(socketDir, entry.Name())
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"} id := wpaSocketCounter.Add(1)
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil { if err != nil {
@@ -45,7 +52,8 @@ func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, err
} }
func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState { func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"} id := wpaSocketCounter.Add(1)
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil { if err != nil {
@@ -85,8 +93,11 @@ func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
} }
} }
// sendCommand sends a command to wpa_supplicant and returns the response.
// Commands are sent without trailing newlines per the wpa_supplicant control
// interface protocol - the official wpa_ctrl.c sends raw command strings.
func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) { func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) {
_, err := conn.Write([]byte(command + "\n")) _, err := conn.Write([]byte(command))
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -121,6 +132,8 @@ func (m *wpaSupplicantMonitor) Start() error {
func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) { func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) {
var consecutiveErrors int var consecutiveErrors int
var debounceTimer *time.Timer
var debounceMutex sync.Mutex
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"} localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
@@ -130,7 +143,14 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
} }
defer conn.Close() defer conn.Close()
_, err = conn.Write([]byte("ATTACH\n")) m.connMutex.Lock()
m.monitorConn = conn
m.connMutex.Unlock()
// ATTACH/DETACH commands use os_strcmp() for exact matching in wpa_supplicant,
// so they must be sent without trailing newlines.
// See: https://w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface_unix.c
_, err = conn.Write([]byte("ATTACH"))
if err != nil { if err != nil {
return return
} }
@@ -144,6 +164,12 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
debounceMutex.Lock()
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceMutex.Unlock()
conn.Write([]byte("DETACH"))
return return
default: default:
} }
@@ -151,6 +177,14 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
conn.SetReadDeadline(time.Now().Add(30 * time.Second)) conn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, err := conn.Read(buf) n, err := conn.Read(buf)
if err != nil { if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
select {
case <-ctx.Done():
return
default:
}
consecutiveErrors++ consecutiveErrors++
if consecutiveErrors > 10 { if consecutiveErrors > 10 {
return return
@@ -162,11 +196,18 @@ func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adap
msg := string(buf[:n]) msg := string(buf[:n])
if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") { if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") {
state := m.ReadWIFIState() debounceMutex.Lock()
if state != lastState { if debounceTimer != nil {
lastState = state debounceTimer.Stop()
m.callback(state)
} }
debounceTimer = time.AfterFunc(500*time.Millisecond, func() {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
})
debounceMutex.Unlock()
} }
} }
} }
@@ -175,5 +216,10 @@ func (m *wpaSupplicantMonitor) Close() error {
if m.cancel != nil { if m.cancel != nil {
m.cancel() m.cancel()
} }
m.connMutex.Lock()
if m.monitorConn != nil {
m.monitorConn.Close()
}
m.connMutex.Unlock()
return nil return nil
} }

View File

@@ -1,4 +1,4 @@
//go:build !linux //go:build !linux && !windows
package settings package settings

View File

@@ -0,0 +1,144 @@
//go:build windows
package settings
import (
"context"
"fmt"
"strings"
"sync"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/winwlanapi"
"golang.org/x/sys/windows"
)
type windowsWIFIMonitor struct {
handle windows.Handle
callback func(adapter.WIFIState)
cancel context.CancelFunc
lastState adapter.WIFIState
mutex sync.Mutex
}
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
handle, err := winwlanapi.OpenHandle()
if err != nil {
return nil, err
}
interfaces, err := winwlanapi.EnumInterfaces(handle)
if err != nil {
winwlanapi.CloseHandle(handle)
return nil, err
}
if len(interfaces) == 0 {
winwlanapi.CloseHandle(handle)
return nil, fmt.Errorf("no wireless interfaces found")
}
return &windowsWIFIMonitor{
handle: handle,
callback: callback,
}, nil
}
func (m *windowsWIFIMonitor) ReadWIFIState() adapter.WIFIState {
interfaces, err := winwlanapi.EnumInterfaces(m.handle)
if err != nil || len(interfaces) == 0 {
return adapter.WIFIState{}
}
for _, iface := range interfaces {
if iface.InterfaceState != winwlanapi.InterfaceStateConnected {
continue
}
guid := iface.InterfaceGUID
attrs, err := winwlanapi.QueryCurrentConnection(m.handle, &guid)
if err != nil {
continue
}
ssidLength := attrs.AssociationAttributes.SSID.Length
if ssidLength == 0 || ssidLength > winwlanapi.Dot11SSIDMaxLength {
continue
}
ssid := string(attrs.AssociationAttributes.SSID.SSID[:ssidLength])
bssid := formatBSSID(attrs.AssociationAttributes.BSSID)
return adapter.WIFIState{
SSID: strings.TrimSpace(ssid),
BSSID: bssid,
}
}
return adapter.WIFIState{}
}
func formatBSSID(mac winwlanapi.Dot11MacAddress) string {
return fmt.Sprintf("%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
}
func (m *windowsWIFIMonitor) Start() error {
if m.callback == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
m.lastState = m.ReadWIFIState()
callbackFunc := func(data *winwlanapi.NotificationData, callbackContext uintptr) uintptr {
if data.NotificationSource != winwlanapi.NotificationSourceACM {
return 0
}
switch data.NotificationCode {
case winwlanapi.NotificationACMConnectionComplete,
winwlanapi.NotificationACMDisconnected:
m.checkAndNotify()
}
return 0
}
callbackPointer := syscall.NewCallback(callbackFunc)
err := winwlanapi.RegisterNotification(m.handle, winwlanapi.NotificationSourceACM, callbackPointer, 0)
if err != nil {
cancel()
return err
}
go func() {
<-ctx.Done()
}()
m.callback(m.lastState)
return nil
}
func (m *windowsWIFIMonitor) checkAndNotify() {
m.mutex.Lock()
defer m.mutex.Unlock()
state := m.ReadWIFIState()
if state != m.lastState {
m.lastState = state
if m.callback != nil {
m.callback(state)
}
}
}
func (m *windowsWIFIMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
winwlanapi.UnregisterNotification(m.handle)
return winwlanapi.CloseHandle(m.handle)
}

View File

@@ -114,13 +114,17 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
switch dnsOptions.Provider { switch dnsOptions.Provider {
case C.DNSProviderAliDNS: case C.DNSProviderAliDNS:
solver.DNSProvider = &alidns.Provider{ solver.DNSProvider = &alidns.Provider{
AccKeyID: dnsOptions.AliDNSOptions.AccessKeyID, CredentialInfo: alidns.CredentialInfo{
AccKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret, AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
RegionID: dnsOptions.AliDNSOptions.RegionID, AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
RegionID: dnsOptions.AliDNSOptions.RegionID,
SecurityToken: dnsOptions.AliDNSOptions.SecurityToken,
},
} }
case C.DNSProviderCloudflare: case C.DNSProviderCloudflare:
solver.DNSProvider = &cloudflare.Provider{ solver.DNSProvider = &cloudflare.Provider{
APIToken: dnsOptions.CloudflareOptions.APIToken, APIToken: dnsOptions.CloudflareOptions.APIToken,
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
} }
default: default:
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider) return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)

View File

@@ -51,6 +51,7 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op
return &ECHClientConfig{ return &ECHClientConfig{
ECHCapableConfig: clientConfig, ECHCapableConfig: clientConfig,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
queryServerName: options.ECH.QueryServerName,
}, nil }, nil
} }
} }
@@ -108,10 +109,11 @@ func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
type ECHClientConfig struct { type ECHClientConfig struct {
ECHCapableConfig ECHCapableConfig
access sync.Mutex access sync.Mutex
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
lastTTL time.Duration queryServerName string
lastUpdate time.Time lastTTL time.Duration
lastUpdate time.Time
} }
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
@@ -130,13 +132,17 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
s.access.Lock() s.access.Lock()
defer s.access.Unlock() defer s.access.Unlock()
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL { if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL {
queryServerName := s.queryServerName
if queryServerName == "" {
queryServerName = s.ServerName()
}
message := &mDNS.Msg{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
}, },
Question: []mDNS.Question{ Question: []mDNS.Question{
{ {
Name: mDNS.Fqdn(s.ServerName()), Name: mDNS.Fqdn(queryServerName),
Qtype: mDNS.TypeHTTPS, Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET, Qclass: mDNS.ClassINET,
}, },
@@ -175,7 +181,12 @@ func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn)
} }
func (s *ECHClientConfig) Clone() Config { func (s *ECHClientConfig) Clone() Config {
return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate} return &ECHClientConfig{
ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig),
dnsRouter: s.dnsRouter,
queryServerName: s.queryServerName,
lastUpdate: s.lastUpdate,
}
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

View File

@@ -169,6 +169,35 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
var clientCertificate []byte
if len(options.ClientCertificate) > 0 {
clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n"))
} else if options.ClientCertificatePath != "" {
content, err := os.ReadFile(options.ClientCertificatePath)
if err != nil {
return nil, E.Cause(err, "read client certificate")
}
clientCertificate = content
}
var clientKey []byte
if len(options.ClientKey) > 0 {
clientKey = []byte(strings.Join(options.ClientKey, "\n"))
} else if options.ClientKeyPath != "" {
content, err := os.ReadFile(options.ClientKeyPath)
if err != nil {
return nil, E.Cause(err, "read client key")
}
clientKey = content
}
if len(clientCertificate) > 0 && len(clientKey) > 0 {
keyPair, err := tls.X509KeyPair(clientCertificate, clientKey)
if err != nil {
return nil, E.Cause(err, "parse client x509 key pair")
}
tlsConfig.Certificates = []tls.Certificate{keyPair}
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
var err error var err error

View File

@@ -222,6 +222,35 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
var clientCertificate []byte
if len(options.ClientCertificate) > 0 {
clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n"))
} else if options.ClientCertificatePath != "" {
content, err := os.ReadFile(options.ClientCertificatePath)
if err != nil {
return nil, E.Cause(err, "read client certificate")
}
clientCertificate = content
}
var clientKey []byte
if len(options.ClientKey) > 0 {
clientKey = []byte(strings.Join(options.ClientKey, "\n"))
} else if options.ClientKeyPath != "" {
content, err := os.ReadFile(options.ClientKeyPath)
if err != nil {
return nil, E.Cause(err, "read client key")
}
clientKey = content
}
if len(clientCertificate) > 0 && len(clientKey) > 0 {
keyPair, err := utls.X509KeyPair(clientCertificate, clientKey)
if err != nil {
return nil, E.Cause(err, "parse client x509 key pair")
}
tlsConfig.Certificates = []utls.Certificate{keyPair}
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
return nil, E.New("client certificate and client key must be provided together")
}
id, err := uTLSClientHelloID(options.UTLS.Fingerprint) id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -14,6 +14,7 @@ import (
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/observable"
) )
var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil) var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
@@ -21,7 +22,7 @@ var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
type HistoryStorage struct { type HistoryStorage struct {
access sync.RWMutex access sync.RWMutex
delayHistory map[string]*adapter.URLTestHistory delayHistory map[string]*adapter.URLTestHistory
updateHook chan<- struct{} updateHook *observable.Subscriber[struct{}]
} }
func NewHistoryStorage() *HistoryStorage { func NewHistoryStorage() *HistoryStorage {
@@ -30,7 +31,7 @@ func NewHistoryStorage() *HistoryStorage {
} }
} }
func (s *HistoryStorage) SetHook(hook chan<- struct{}) { func (s *HistoryStorage) SetHook(hook *observable.Subscriber[struct{}]) {
s.updateHook = hook s.updateHook = hook
} }
@@ -60,10 +61,7 @@ func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTes
func (s *HistoryStorage) notifyUpdated() { func (s *HistoryStorage) notifyUpdated() {
updateHook := s.updateHook updateHook := s.updateHook
if updateHook != nil { if updateHook != nil {
select { updateHook.Emit(struct{}{})
case updateHook <- struct{}{}:
default:
}
} }
} }

View File

@@ -3,5 +3,6 @@ package constant
const ( const (
CertificateStoreSystem = "system" CertificateStoreSystem = "system"
CertificateStoreMozilla = "mozilla" CertificateStoreMozilla = "mozilla"
CertificateStoreChrome = "chrome"
CertificateStoreNone = "none" CertificateStoreNone = "none"
) )

View File

@@ -4,5 +4,5 @@ import "time"
const ( const (
DHCPTTL = time.Hour DHCPTTL = time.Hour
DHCPTimeout = time.Minute DHCPTimeout = 5 * time.Second
) )

View File

@@ -28,6 +28,8 @@ const (
TypeDERP = "derp" TypeDERP = "derp"
TypeResolved = "resolved" TypeResolved = "resolved"
TypeSSMAPI = "ssm-api" TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
) )
const ( const (

View File

@@ -30,6 +30,7 @@ const (
RuleActionTypeRoute = "route" RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options" RuleActionTypeRouteOptions = "route-options"
RuleActionTypeDirect = "direct" RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject" RuleActionTypeReject = "reject"
RuleActionTypeHijackDNS = "hijack-dns" RuleActionTypeHijackDNS = "hijack-dns"
RuleActionTypeSniff = "sniff" RuleActionTypeSniff = "sniff"

View File

@@ -3,7 +3,7 @@ package constant
import "time" import "time"
const ( const (
TCPKeepAliveInitial = 10 * time.Minute TCPKeepAliveInitial = 5 * time.Minute
TCPKeepAliveInterval = 75 * time.Second TCPKeepAliveInterval = 75 * time.Second
TCPConnectTimeout = 5 * time.Second TCPConnectTimeout = 5 * time.Second
TCPTimeout = 15 * time.Second TCPTimeout = 15 * time.Second

View File

@@ -7,15 +7,12 @@ import (
"github.com/sagernet/sing-box" "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
) )
@@ -29,23 +26,12 @@ type Instance struct {
urlTestHistoryStorage *urltest.HistoryStorage urlTestHistoryStorage *urltest.HistoryStorage
} }
func (s *StartedService) baseContext() context.Context {
dnsRegistry := include.DNSTransportRegistry()
if s.platform != nil && s.platform.UsePlatformLocalDNSTransport() {
dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, s.platform.LocalDNSTransport())
}
ctx := box.Context(s.ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
ctx = filemanager.WithDefault(ctx, s.workingDirectory, s.tempDirectory, s.userID, s.groupID)
return ctx
}
func (s *StartedService) CheckConfig(configContent string) error { func (s *StartedService) CheckConfig(configContent string) error {
ctx := s.baseContext() options, err := parseConfig(s.ctx, configContent)
options, err := parseConfig(ctx, configContent)
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(s.ctx)
defer cancel() defer cancel()
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
@@ -58,7 +44,7 @@ func (s *StartedService) CheckConfig(configContent string) error {
} }
func (s *StartedService) FormatConfig(configContent string) (string, error) { func (s *StartedService) FormatConfig(configContent string) (string, error) {
options, err := parseConfig(s.baseContext(), configContent) options, err := parseConfig(s.ctx, configContent)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -79,7 +65,7 @@ type OverrideOptions struct {
} }
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := s.baseContext() ctx := s.ctx
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
ctx, cancel := context.WithCancel(include.Context(ctx)) ctx, cancel := context.WithCancel(include.Context(ctx))
options, err := parseConfig(ctx, profileContent) options, err := parseConfig(ctx, profileContent)

View File

@@ -1,11 +1,5 @@
package daemon package daemon
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/option"
)
type PlatformHandler interface { type PlatformHandler interface {
ServiceStop() error ServiceStop() error
ServiceReload() error ServiceReload() error
@@ -13,10 +7,3 @@ type PlatformHandler interface {
SetSystemProxyEnabled(enabled bool) error SetSystemProxyEnabled(enabled bool) error
WriteDebugMessage(message string) WriteDebugMessage(message string)
} }
type PlatformInterface interface {
adapter.PlatformInterface
UsePlatformLocalDNSTransport() bool
LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions]
}

View File

@@ -31,16 +31,16 @@ import (
var _ StartedServiceServer = (*StartedService)(nil) var _ StartedServiceServer = (*StartedService)(nil)
type StartedService struct { type StartedService struct {
ctx context.Context ctx context.Context
platform PlatformInterface // platform adapter.PlatformInterface
platformHandler PlatformHandler handler PlatformHandler
debug bool debug bool
logMaxLines int logMaxLines int
workingDirectory string // workingDirectory string
tempDirectory string // tempDirectory string
userID int // userID int
groupID int // groupID int
systemProxyEnabled bool // systemProxyEnabled bool
serviceAccess sync.RWMutex serviceAccess sync.RWMutex
serviceStatus *ServiceStatus serviceStatus *ServiceStatus
serviceStatusSubscriber *observable.Subscriber[*ServiceStatus] serviceStatusSubscriber *observable.Subscriber[*ServiceStatus]
@@ -50,6 +50,7 @@ type StartedService struct {
logSubscriber *observable.Subscriber[*log.Entry] logSubscriber *observable.Subscriber[*log.Entry]
logObserver *observable.Observer[*log.Entry] logObserver *observable.Observer[*log.Entry]
instance *Instance instance *Instance
startedAt time.Time
urlTestSubscriber *observable.Subscriber[struct{}] urlTestSubscriber *observable.Subscriber[struct{}]
urlTestObserver *observable.Observer[struct{}] urlTestObserver *observable.Observer[struct{}]
urlTestHistoryStorage *urltest.HistoryStorage urlTestHistoryStorage *urltest.HistoryStorage
@@ -58,30 +59,30 @@ type StartedService struct {
} }
type ServiceOptions struct { type ServiceOptions struct {
Context context.Context Context context.Context
Platform PlatformInterface // Platform adapter.PlatformInterface
PlatformHandler PlatformHandler Handler PlatformHandler
Debug bool Debug bool
LogMaxLines int LogMaxLines int
WorkingDirectory string // WorkingDirectory string
TempDirectory string // TempDirectory string
UserID int // UserID int
GroupID int // GroupID int
SystemProxyEnabled bool // SystemProxyEnabled bool
} }
func NewStartedService(options ServiceOptions) *StartedService { func NewStartedService(options ServiceOptions) *StartedService {
s := &StartedService{ s := &StartedService{
ctx: options.Context, ctx: options.Context,
platform: options.Platform, // platform: options.Platform,
platformHandler: options.PlatformHandler, handler: options.Handler,
debug: options.Debug, debug: options.Debug,
logMaxLines: options.LogMaxLines, logMaxLines: options.LogMaxLines,
workingDirectory: options.WorkingDirectory, // workingDirectory: options.WorkingDirectory,
tempDirectory: options.TempDirectory, // tempDirectory: options.TempDirectory,
userID: options.UserID, // userID: options.UserID,
groupID: options.GroupID, // groupID: options.GroupID,
systemProxyEnabled: options.SystemProxyEnabled, // systemProxyEnabled: options.SystemProxyEnabled,
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE}, serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4), serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
logSubscriber: observable.NewSubscriber[*log.Entry](128), logSubscriber: observable.NewSubscriber[*log.Entry](128),
@@ -117,6 +118,46 @@ func (s *StartedService) updateStatusError(err error) error {
return err return err
} }
func (s *StartedService) waitForStarted(ctx context.Context) error {
s.serviceAccess.RLock()
currentStatus := s.serviceStatus.Status
s.serviceAccess.RUnlock()
switch currentStatus {
case ServiceStatus_STARTED:
return nil
case ServiceStatus_STARTING:
default:
return os.ErrInvalid
}
subscription, done, err := s.serviceStatusObserver.Subscribe()
if err != nil {
return err
}
defer s.serviceStatusObserver.UnSubscribe(subscription)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-s.ctx.Done():
return s.ctx.Err()
case status := <-subscription:
switch status.Status {
case ServiceStatus_STARTED:
return nil
case ServiceStatus_FATAL:
return E.New(status.ErrorMessage)
case ServiceStatus_IDLE, ServiceStatus_STOPPING:
return os.ErrInvalid
}
case <-done:
return os.ErrClosed
}
}
}
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error { func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
s.serviceAccess.Lock() s.serviceAccess.Lock()
switch s.serviceStatus.Status { switch s.serviceStatus.Status {
@@ -125,6 +166,13 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
return os.ErrInvalid return os.ErrInvalid
} }
oldInstance := s.instance
if oldInstance != nil {
s.updateStatus(ServiceStatus_STOPPING)
s.serviceAccess.Unlock()
_ = oldInstance.Close()
s.serviceAccess.Lock()
}
s.updateStatus(ServiceStatus_STARTING) s.updateStatus(ServiceStatus_STARTING)
s.resetLogs() s.resetLogs()
instance, err := s.newInstance(profileContent, options) instance, err := s.newInstance(profileContent, options)
@@ -132,6 +180,10 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
return s.updateStatusError(err) return s.updateStatusError(err)
} }
s.instance = instance s.instance = instance
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
if instance.clashServer != nil {
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
}
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
err = instance.Start() err = instance.Start()
s.serviceAccess.Lock() s.serviceAccess.Lock()
@@ -142,6 +194,7 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
if err != nil { if err != nil {
return s.updateStatusError(err) return s.updateStatusError(err)
} }
s.startedAt = time.Now()
s.updateStatus(ServiceStatus_STARTED) s.updateStatus(ServiceStatus_STARTED)
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
runtime.GC() runtime.GC()
@@ -164,6 +217,7 @@ func (s *StartedService) CloseService() error {
} }
} }
s.instance = nil s.instance = nil
s.startedAt = time.Time{}
s.updateStatus(ServiceStatus_IDLE) s.updateStatus(ServiceStatus_IDLE)
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
runtime.GC() runtime.GC()
@@ -173,12 +227,11 @@ func (s *StartedService) CloseService() error {
func (s *StartedService) SetError(err error) { func (s *StartedService) SetError(err error) {
s.serviceAccess.Lock() s.serviceAccess.Lock()
s.updateStatusError(err) s.updateStatusError(err)
s.serviceAccess.Unlock()
s.WriteMessage(log.LevelError, err.Error()) s.WriteMessage(log.LevelError, err.Error())
} }
func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.platformHandler.ServiceStop() err := s.handler.ServiceStop()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -186,7 +239,7 @@ func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty)
} }
func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
err := s.platformHandler.ServiceReload() err := s.handler.ServiceReload()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -227,8 +280,8 @@ func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerSt
for element := s.logLines.Front(); element != nil; element = element.Next() { for element := s.logLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value) savedLines = append(savedLines, element.Value)
} }
s.logAccess.Unlock()
subscription, done, err := s.logObserver.Subscribe() subscription, done, err := s.logObserver.Subscribe()
s.logAccess.Unlock()
if err != nil { if err != nil {
return err return err
} }
@@ -252,30 +305,33 @@ func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerSt
case <-server.Context().Done(): case <-server.Context().Done():
return server.Context().Err() return server.Context().Err()
case message := <-subscription: case message := <-subscription:
var rawMessage Log
if message == nil { if message == nil {
err = server.Send(&Log{Reset_: true}) rawMessage.Reset_ = true
if err != nil { } else {
return err rawMessage.Messages = append(rawMessage.Messages, &Log_Message{
} Level: LogLevel(message.Level),
continue Message: message.Message,
})
} }
messages := []*Log_Message{{
Level: LogLevel(message.Level),
Message: message.Message,
}}
fetch: fetch:
for { for {
select { select {
case message = <-subscription: case message = <-subscription:
messages = append(messages, &Log_Message{ if message == nil {
Level: LogLevel(message.Level), rawMessage.Messages = nil
Message: message.Message, rawMessage.Reset_ = true
}) } else {
rawMessage.Messages = append(rawMessage.Messages, &Log_Message{
Level: LogLevel(message.Level),
Message: message.Message,
})
}
default: default:
break fetch break fetch
} }
} }
err = server.Send(&Log{Messages: messages}) err = server.Send(&rawMessage)
if err != nil { if err != nil {
return err return err
} }
@@ -298,6 +354,11 @@ func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.
return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil
} }
func (s *StartedService) ClearLogs(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
s.resetLogs()
return &emptypb.Empty{}, nil
}
func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error { func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error {
interval := time.Duration(request.Interval) interval := time.Duration(request.Interval)
if interval <= 0 { if interval <= 0 {
@@ -335,7 +396,9 @@ func (s *StartedService) readStatus() *Status {
status.Memory = memory.Inuse() status.Memory = memory.Inuse()
status.Goroutines = int32(runtime.NumGoroutine()) status.Goroutines = int32(runtime.NumGoroutine())
status.ConnectionsOut = int32(conntrack.Count()) status.ConnectionsOut = int32(conntrack.Count())
s.serviceAccess.RLock()
nowService := s.instance nowService := s.instance
s.serviceAccess.RUnlock()
if nowService != nil { if nowService != nil {
if clashServer := nowService.clashServer; clashServer != nil { if clashServer := nowService.clashServer; clashServer != nil {
status.TrafficAvailable = true status.TrafficAvailable = true
@@ -348,6 +411,10 @@ func (s *StartedService) readStatus() *Status {
} }
func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error { func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.urlTestObserver.Subscribe() subscription, done, err := s.urlTestObserver.Subscribe()
if err != nil { if err != nil {
return err return err
@@ -355,18 +422,16 @@ func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.Serve
defer s.urlTestObserver.UnSubscribe(subscription) defer s.urlTestObserver.UnSubscribe(subscription)
for { for {
s.serviceAccess.RLock() s.serviceAccess.RLock()
switch s.serviceStatus.Status { if s.serviceStatus.Status != ServiceStatus_STARTED {
case ServiceStatus_STARTING, ServiceStatus_STARTED:
groups := s.readGroups()
s.serviceAccess.RUnlock()
err = server.Send(groups)
if err != nil {
return err
}
default:
s.serviceAccess.RUnlock() s.serviceAccess.RUnlock()
return os.ErrInvalid return os.ErrInvalid
} }
groups := s.readGroups()
s.serviceAccess.RUnlock()
err = server.Send(groups)
if err != nil {
return err
}
select { select {
case <-subscription: case <-subscription:
case <-s.ctx.Done(): case <-s.ctx.Done():
@@ -443,12 +508,27 @@ func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.
} }
func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error { func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.clashModeObserver.Subscribe() subscription, done, err := s.clashModeObserver.Subscribe()
if err != nil { if err != nil {
return err return err
} }
defer s.clashModeObserver.UnSubscribe(subscription) defer s.clashModeObserver.UnSubscribe(subscription)
for { for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
message := &ClashMode{Mode: s.instance.clashServer.Mode()}
s.serviceAccess.RUnlock()
err = server.Send(message)
if err != nil {
return err
}
select { select {
case <-subscription: case <-subscription:
case <-s.ctx.Done(): case <-s.ctx.Done():
@@ -458,16 +538,6 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
case <-done: case <-done:
return nil return nil
} }
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
return nil
}
message := &ClashMode{Mode: s.instance.clashServer.Mode()}
s.serviceAccess.RUnlock()
err = server.Send(message)
if err != nil {
return err
}
} }
} }
@@ -504,12 +574,7 @@ func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (
if isURLTest { if isURLTest {
go urlTest.CheckOutbounds() go urlTest.CheckOutbounds()
} else { } else {
var historyStorage adapter.URLTestHistoryStorage historyStorage := boxService.urlTestHistoryStorage
if s.instance.clashServer != nil {
historyStorage = s.instance.clashServer.HistoryStorage()
} else {
return nil, E.New("Clash API is required for URLTest on non-URLTest group")
}
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := boxService.instance.Outbound().Outbound(it) itOutbound, _ := boxService.instance.Outbound().Outbound(it)
@@ -566,6 +631,7 @@ func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutb
if !selector.SelectOutbound(request.OutboundTag) { if !selector.SelectOutbound(request.OutboundTag) {
return nil, E.New("outbound not found in selector: ", request.OutboundTag) return nil, E.New("outbound not found in selector: ", request.OutboundTag)
} }
s.urlTestObserver.Emit(struct{}{})
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
@@ -589,11 +655,11 @@ func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupEx
} }
func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) { func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
return s.platformHandler.SystemProxyStatus() return s.handler.SystemProxyStatus()
} }
func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
err := s.platformHandler.SetSystemProxyEnabled(request.Enabled) err := s.handler.SetSystemProxyEnabled(request.Enabled)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -601,13 +667,11 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
} }
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error { func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
s.serviceAccess.RLock() err := s.waitForStarted(server.Context())
switch s.serviceStatus.Status { if err != nil {
case ServiceStatus_STARTING, ServiceStatus_STARTED: return err
default:
s.serviceAccess.RUnlock()
return os.ErrInvalid
} }
s.serviceAccess.RLock()
boxService := s.instance boxService := s.instance
s.serviceAccess.RUnlock() s.serviceAccess.RUnlock()
ticker := time.NewTicker(time.Duration(request.Interval)) ticker := time.NewTicker(time.Duration(request.Interval))
@@ -673,6 +737,16 @@ func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol
uplink = 0 uplink = 0
downlink = 0 downlink = 0
} }
var processInfo *ProcessInfo
if metadata.Metadata.ProcessInfo != nil {
processInfo = &ProcessInfo{
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
UserId: metadata.Metadata.ProcessInfo.UserId,
UserName: metadata.Metadata.ProcessInfo.UserName,
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
}
}
connection := &Connection{ connection := &Connection{
Id: metadata.ID.String(), Id: metadata.ID.String(),
Inbound: metadata.Metadata.Inbound, Inbound: metadata.Metadata.Inbound,
@@ -695,6 +769,7 @@ func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol
Outbound: metadata.Outbound, Outbound: metadata.Outbound,
OutboundType: metadata.OutboundType, OutboundType: metadata.OutboundType,
ChainList: metadata.Chain, ChainList: metadata.Chain,
ProcessInfo: processInfo,
} }
connections[metadata.ID] = connection connections[metadata.ID] = connection
return connection return connection
@@ -742,6 +817,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
}, nil }, nil
} }
func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty) (*StartedAt, error) {
s.serviceAccess.RLock()
defer s.serviceAccess.RUnlock()
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
}
func (s *StartedService) SubscribeHelperEvents(empty *emptypb.Empty, server grpc.ServerStreamingServer[HelperRequest]) error { func (s *StartedService) SubscribeHelperEvents(empty *emptypb.Empty, server grpc.ServerStreamingServer[HelperRequest]) error {
return os.ErrInvalid return os.ErrInvalid
} }
@@ -755,15 +836,15 @@ func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
func (s *StartedService) WriteMessage(level log.Level, message string) { func (s *StartedService) WriteMessage(level log.Level, message string) {
item := &log.Entry{Level: level, Message: message} item := &log.Entry{Level: level, Message: message}
s.logSubscriber.Emit(item)
s.logAccess.Lock() s.logAccess.Lock()
s.logLines.PushBack(item) s.logLines.PushBack(item)
if s.logLines.Len() > s.logMaxLines { if s.logLines.Len() > s.logMaxLines {
s.logLines.Remove(s.logLines.Front()) s.logLines.Remove(s.logLines.Front())
} }
s.logAccess.Unlock() s.logAccess.Unlock()
s.logSubscriber.Emit(item)
if s.debug { if s.debug {
s.platformHandler.WriteDebugMessage(message) s.handler.WriteDebugMessage(message)
} }
} }

View File

@@ -1238,6 +1238,7 @@ type Connection struct {
Outbound string `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"` Outbound string `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"`
OutboundType string `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"` OutboundType string `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"`
ChainList []string `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"` ChainList []string `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"`
ProcessInfo *ProcessInfo `protobuf:"bytes,22,opt,name=processInfo,proto3" json:"processInfo,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -1419,6 +1420,89 @@ func (x *Connection) GetChainList() []string {
return nil return nil
} }
func (x *Connection) GetProcessInfo() *ProcessInfo {
if x != nil {
return x.ProcessInfo
}
return nil
}
type ProcessInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
ProcessId uint32 `protobuf:"varint,1,opt,name=processId,proto3" json:"processId,omitempty"`
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ProcessInfo) Reset() {
*x = ProcessInfo{}
mi := &file_daemon_started_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ProcessInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProcessInfo) ProtoMessage() {}
func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[19]
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 ProcessInfo.ProtoReflect.Descriptor instead.
func (*ProcessInfo) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
}
func (x *ProcessInfo) GetProcessId() uint32 {
if x != nil {
return x.ProcessId
}
return 0
}
func (x *ProcessInfo) GetUserId() int32 {
if x != nil {
return x.UserId
}
return 0
}
func (x *ProcessInfo) GetUserName() string {
if x != nil {
return x.UserName
}
return ""
}
func (x *ProcessInfo) GetProcessPath() string {
if x != nil {
return x.ProcessPath
}
return ""
}
func (x *ProcessInfo) GetPackageName() string {
if x != nil {
return x.PackageName
}
return ""
}
type CloseConnectionRequest struct { type CloseConnectionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -1428,7 +1512,7 @@ type CloseConnectionRequest struct {
func (x *CloseConnectionRequest) Reset() { func (x *CloseConnectionRequest) Reset() {
*x = CloseConnectionRequest{} *x = CloseConnectionRequest{}
mi := &file_daemon_started_service_proto_msgTypes[19] mi := &file_daemon_started_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1440,7 +1524,7 @@ func (x *CloseConnectionRequest) String() string {
func (*CloseConnectionRequest) ProtoMessage() {} func (*CloseConnectionRequest) ProtoMessage() {}
func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[19] mi := &file_daemon_started_service_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1453,7 +1537,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead.
func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { func (*CloseConnectionRequest) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{19} return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
} }
func (x *CloseConnectionRequest) GetId() string { func (x *CloseConnectionRequest) GetId() string {
@@ -1472,7 +1556,7 @@ type DeprecatedWarnings struct {
func (x *DeprecatedWarnings) Reset() { func (x *DeprecatedWarnings) Reset() {
*x = DeprecatedWarnings{} *x = DeprecatedWarnings{}
mi := &file_daemon_started_service_proto_msgTypes[20] mi := &file_daemon_started_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1484,7 +1568,7 @@ func (x *DeprecatedWarnings) String() string {
func (*DeprecatedWarnings) ProtoMessage() {} func (*DeprecatedWarnings) ProtoMessage() {}
func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[20] mi := &file_daemon_started_service_proto_msgTypes[21]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1497,7 +1581,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead. // Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead.
func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { func (*DeprecatedWarnings) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{20} return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
} }
func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
@@ -1518,7 +1602,7 @@ type DeprecatedWarning struct {
func (x *DeprecatedWarning) Reset() { func (x *DeprecatedWarning) Reset() {
*x = DeprecatedWarning{} *x = DeprecatedWarning{}
mi := &file_daemon_started_service_proto_msgTypes[21] mi := &file_daemon_started_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1530,7 +1614,7 @@ func (x *DeprecatedWarning) String() string {
func (*DeprecatedWarning) ProtoMessage() {} func (*DeprecatedWarning) ProtoMessage() {}
func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[21] mi := &file_daemon_started_service_proto_msgTypes[22]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1543,7 +1627,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead. // Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead.
func (*DeprecatedWarning) Descriptor() ([]byte, []int) { func (*DeprecatedWarning) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{21} return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
} }
func (x *DeprecatedWarning) GetMessage() string { func (x *DeprecatedWarning) GetMessage() string {
@@ -1567,6 +1651,50 @@ func (x *DeprecatedWarning) GetMigrationLink() string {
return "" return ""
} }
type StartedAt struct {
state protoimpl.MessageState `protogen:"open.v1"`
StartedAt int64 `protobuf:"varint,1,opt,name=startedAt,proto3" json:"startedAt,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StartedAt) Reset() {
*x = StartedAt{}
mi := &file_daemon_started_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StartedAt) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartedAt) ProtoMessage() {}
func (x *StartedAt) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[23]
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 StartedAt.ProtoReflect.Descriptor instead.
func (*StartedAt) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
}
func (x *StartedAt) GetStartedAt() int64 {
if x != nil {
return x.StartedAt
}
return 0
}
type Log_Message struct { type Log_Message struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"`
@@ -1577,7 +1705,7 @@ type Log_Message struct {
func (x *Log_Message) Reset() { func (x *Log_Message) Reset() {
*x = Log_Message{} *x = Log_Message{}
mi := &file_daemon_started_service_proto_msgTypes[22] mi := &file_daemon_started_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1589,7 +1717,7 @@ func (x *Log_Message) String() string {
func (*Log_Message) ProtoMessage() {} func (*Log_Message) ProtoMessage() {}
func (x *Log_Message) ProtoReflect() protoreflect.Message { func (x *Log_Message) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[22] mi := &file_daemon_started_service_proto_msgTypes[24]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1696,7 +1824,7 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" + "\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" +
"\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" + "\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" +
"\vConnections\x124\n" + "\vConnections\x124\n" +
"\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\xde\x04\n" + "\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\x95\x05\n" +
"\n" + "\n" +
"Connection\x12\x0e\n" + "Connection\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
@@ -1720,7 +1848,14 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" + "\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" +
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" + "\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" + "\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
"\tchainList\x18\x15 \x03(\tR\tchainList\"(\n" + "\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" +
"\vProcessInfo\x12\x1c\n" +
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
"\x16CloseConnectionRequest\x12\x0e\n" + "\x16CloseConnectionRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
"\x12DeprecatedWarnings\x125\n" + "\x12DeprecatedWarnings\x125\n" +
@@ -1728,7 +1863,9 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x11DeprecatedWarning\x12\x18\n" + "\x11DeprecatedWarning\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" +
"\timpending\x18\x02 \x01(\bR\timpending\x12$\n" + "\timpending\x18\x02 \x01(\bR\timpending\x12$\n" +
"\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink*U\n" + "\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\")\n" +
"\tStartedAt\x12\x1c\n" +
"\tstartedAt\x18\x01 \x01(\x03R\tstartedAt*U\n" +
"\bLogLevel\x12\t\n" + "\bLogLevel\x12\t\n" +
"\x05PANIC\x10\x00\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" +
"\x05FATAL\x10\x01\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" +
@@ -1746,13 +1883,14 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x10ConnectionSortBy\x12\b\n" + "\x10ConnectionSortBy\x12\b\n" +
"\x04DATE\x10\x00\x12\v\n" + "\x04DATE\x10\x00\x12\v\n" +
"\aTRAFFIC\x10\x01\x12\x11\n" + "\aTRAFFIC\x10\x01\x12\x11\n" +
"\rTOTAL_TRAFFIC\x10\x022\xf8\v\n" + "\rTOTAL_TRAFFIC\x10\x022\xf4\f\n" +
"\x0eStartedService\x12=\n" + "\x0eStartedService\x12=\n" +
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\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" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
"\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" + "\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" +
"\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" + "\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" +
"\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12E\n" + "\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12=\n" +
"\tClearLogs\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12E\n" +
"\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" + "\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" +
"\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" + "\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" +
"\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" + "\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" +
@@ -1766,7 +1904,8 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x13.daemon.Connections\"\x000\x01\x12K\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" + "\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" + "\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\x12J\n" + "\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" +
"\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12J\n" +
"\x15SubscribeHelperEvents\x12\x16.google.protobuf.Empty\x1a\x15.daemon.HelperRequest\"\x000\x01\x12F\n" + "\x15SubscribeHelperEvents\x12\x16.google.protobuf.Empty\x1a\x15.daemon.HelperRequest\"\x000\x01\x12F\n" +
"\x12SendHelperResponse\x12\x16.daemon.HelperResponse\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" "\x12SendHelperResponse\x12\x16.daemon.HelperResponse\x1a\x16.google.protobuf.Empty\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
@@ -1784,7 +1923,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
var ( var (
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 23) file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
file_daemon_started_service_proto_goTypes = []any{ file_daemon_started_service_proto_goTypes = []any{
(LogLevel)(0), // 0: daemon.LogLevel (LogLevel)(0), // 0: daemon.LogLevel
(ConnectionFilter)(0), // 1: daemon.ConnectionFilter (ConnectionFilter)(0), // 1: daemon.ConnectionFilter
@@ -1809,74 +1948,81 @@ var (
(*SubscribeConnectionsRequest)(nil), // 20: daemon.SubscribeConnectionsRequest (*SubscribeConnectionsRequest)(nil), // 20: daemon.SubscribeConnectionsRequest
(*Connections)(nil), // 21: daemon.Connections (*Connections)(nil), // 21: daemon.Connections
(*Connection)(nil), // 22: daemon.Connection (*Connection)(nil), // 22: daemon.Connection
(*CloseConnectionRequest)(nil), // 23: daemon.CloseConnectionRequest (*ProcessInfo)(nil), // 23: daemon.ProcessInfo
(*DeprecatedWarnings)(nil), // 24: daemon.DeprecatedWarnings (*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest
(*DeprecatedWarning)(nil), // 25: daemon.DeprecatedWarning (*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings
(*Log_Message)(nil), // 26: daemon.Log.Message (*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning
(*emptypb.Empty)(nil), // 27: google.protobuf.Empty (*StartedAt)(nil), // 27: daemon.StartedAt
(*HelperResponse)(nil), // 28: daemon.HelperResponse (*Log_Message)(nil), // 28: daemon.Log.Message
(*HelperRequest)(nil), // 29: daemon.HelperRequest (*emptypb.Empty)(nil), // 29: google.protobuf.Empty
(*HelperResponse)(nil), // 30: daemon.HelperResponse
(*HelperRequest)(nil), // 31: daemon.HelperRequest
} }
) )
var file_daemon_started_service_proto_depIdxs = []int32{ var file_daemon_started_service_proto_depIdxs = []int32{
3, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type 3, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
26, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
11, // 3: daemon.Groups.group:type_name -> daemon.Group 11, // 3: daemon.Groups.group:type_name -> daemon.Group
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
1, // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter 1, // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter
2, // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy 2, // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy
22, // 7: daemon.Connections.connections:type_name -> daemon.Connection 22, // 7: daemon.Connections.connections:type_name -> daemon.Connection
25, // 8: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning 23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
0, // 9: daemon.Log.Message.level:type_name -> daemon.LogLevel 26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
27, // 10: daemon.StartedService.StopService:input_type -> google.protobuf.Empty 0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel
27, // 11: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty 29, // 11: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
27, // 12: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty 29, // 12: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
27, // 13: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty 29, // 13: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
27, // 14: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty 29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
6, // 15: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest 29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
27, // 16: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty 29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
27, // 17: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty 6, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
27, // 18: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty 29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
16, // 19: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode 29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
13, // 20: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest 29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
14, // 21: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest 16, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
15, // 22: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest 13, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
27, // 23: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty 14, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
19, // 24: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest 15, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
20, // 25: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest 29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
23, // 26: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest 19, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
27, // 27: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty 20, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
27, // 28: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty 24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
27, // 29: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty 29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
28, // 30: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse 29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
27, // 31: daemon.StartedService.StopService:output_type -> google.protobuf.Empty 29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
27, // 32: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty 29, // 32: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty
4, // 33: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus 30, // 33: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse
7, // 34: daemon.StartedService.SubscribeLog:output_type -> daemon.Log 29, // 34: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
8, // 35: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel 29, // 35: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
9, // 36: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status 4, // 36: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
10, // 37: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups 7, // 37: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
17, // 38: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus 8, // 38: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
16, // 39: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode 29, // 39: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
27, // 40: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty 9, // 40: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
27, // 41: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty 10, // 41: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
27, // 42: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty 17, // 42: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
27, // 43: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty 16, // 43: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
18, // 44: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus 29, // 44: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
27, // 45: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty 29, // 45: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
21, // 46: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections 29, // 46: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
27, // 47: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty 29, // 47: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
27, // 48: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty 18, // 48: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
24, // 49: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings 29, // 49: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
29, // 50: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest 21, // 50: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
27, // 51: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty 29, // 51: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
31, // [31:52] is the sub-list for method output_type 29, // 52: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
10, // [10:31] is the sub-list for method input_type 25, // 53: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
10, // [10:10] is the sub-list for extension type_name 27, // 54: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
10, // [10:10] is the sub-list for extension extendee 31, // 55: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest
0, // [0:10] is the sub-list for field type_name 29, // 56: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty
34, // [34:57] is the sub-list for method output_type
11, // [11:34] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
} }
func init() { file_daemon_started_service_proto_init() } func init() { file_daemon_started_service_proto_init() }
@@ -1891,7 +2037,7 @@ func file_daemon_started_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
NumEnums: 4, NumEnums: 4,
NumMessages: 23, NumMessages: 25,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -13,6 +13,7 @@ service StartedService {
rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {} rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {}
rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {} rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {}
rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {} rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {}
rpc ClearLogs(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {} rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {}
rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {} rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {}
@@ -31,6 +32,7 @@ service StartedService {
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {} rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {} rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {} rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
rpc SubscribeHelperEvents(google.protobuf.Empty) returns(stream HelperRequest) {} rpc SubscribeHelperEvents(google.protobuf.Empty) returns(stream HelperRequest) {}
rpc SendHelperResponse(HelperResponse) returns(google.protobuf.Empty) {} rpc SendHelperResponse(HelperResponse) returns(google.protobuf.Empty) {}
@@ -187,6 +189,15 @@ message Connection {
string outbound = 19; string outbound = 19;
string outboundType = 20; string outboundType = 20;
repeated string chainList = 21; repeated string chainList = 21;
ProcessInfo processInfo = 22;
}
message ProcessInfo {
uint32 processId = 1;
int32 userId = 2;
string userName = 3;
string processPath = 4;
string packageName = 5;
} }
message CloseConnectionRequest { message CloseConnectionRequest {
@@ -201,4 +212,8 @@ message DeprecatedWarning {
string message = 1; string message = 1;
bool impending = 2; bool impending = 2;
string migrationLink = 3; string migrationLink = 3;
}
message StartedAt {
int64 startedAt = 1;
} }

View File

@@ -20,6 +20,7 @@ const (
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
@@ -34,6 +35,7 @@ const (
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_SubscribeHelperEvents_FullMethodName = "/daemon.StartedService/SubscribeHelperEvents" StartedService_SubscribeHelperEvents_FullMethodName = "/daemon.StartedService/SubscribeHelperEvents"
StartedService_SendHelperResponse_FullMethodName = "/daemon.StartedService/SendHelperResponse" StartedService_SendHelperResponse_FullMethodName = "/daemon.StartedService/SendHelperResponse"
) )
@@ -47,6 +49,7 @@ type StartedServiceClient interface {
SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error)
SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error)
GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error)
ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error)
SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error)
GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error)
@@ -61,6 +64,7 @@ type StartedServiceClient interface {
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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) 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) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error)
SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error) SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error)
} }
@@ -141,6 +145,16 @@ func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *empty
return out, nil return out, nil
} }
func (c *startedServiceClient) ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_ClearLogs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) { func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...)
@@ -317,6 +331,16 @@ func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *em
return out, nil return out, nil
} }
func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StartedAt)
err := c.cc.Invoke(ctx, StartedService_GetStartedAt_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) { func (c *startedServiceClient) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeHelperEvents_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeHelperEvents_FullMethodName, cOpts...)
@@ -355,6 +379,7 @@ type StartedServiceServer interface {
SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error
SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error
GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error)
ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error
SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error
GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error)
@@ -369,6 +394,7 @@ type StartedServiceServer interface {
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error
SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error) SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error)
mustEmbedUnimplementedStartedServiceServer() mustEmbedUnimplementedStartedServiceServer()
@@ -401,6 +427,10 @@ func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *em
return nil, status.Errorf(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.Errorf(codes.Unimplemented, "method ClearLogs not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error { func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
} }
@@ -457,6 +487,10 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context,
return nil, status.Errorf(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.Errorf(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error { func (UnimplementedStartedServiceServer) SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeHelperEvents not implemented") return status.Errorf(codes.Unimplemented, "method SubscribeHelperEvents not implemented")
} }
@@ -561,6 +595,24 @@ func _StartedService_GetDefaultLogLevel_Handler(srv interface{}, ctx context.Con
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StartedService_ClearLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).ClearLogs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_ClearLogs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).ClearLogs(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error { func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeStatusRequest) m := new(SubscribeStatusRequest)
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
@@ -785,6 +837,24 @@ func _StartedService_GetDeprecatedWarnings_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).GetStartedAt(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_GetStartedAt_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).GetStartedAt(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeHelperEvents_Handler(srv interface{}, stream grpc.ServerStream) error { func _StartedService_SubscribeHelperEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty) m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
@@ -833,6 +903,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetDefaultLogLevel", MethodName: "GetDefaultLogLevel",
Handler: _StartedService_GetDefaultLogLevel_Handler, Handler: _StartedService_GetDefaultLogLevel_Handler,
}, },
{
MethodName: "ClearLogs",
Handler: _StartedService_ClearLogs_Handler,
},
{ {
MethodName: "GetClashModeStatus", MethodName: "GetClashModeStatus",
Handler: _StartedService_GetClashModeStatus_Handler, Handler: _StartedService_GetClashModeStatus_Handler,
@@ -873,6 +947,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetDeprecatedWarnings", MethodName: "GetDeprecatedWarnings",
Handler: _StartedService_GetDeprecatedWarnings_Handler, Handler: _StartedService_GetDeprecatedWarnings_Handler,
}, },
{
MethodName: "GetStartedAt",
Handler: _StartedService_GetStartedAt_Handler,
},
{ {
MethodName: "SendHelperResponse", MethodName: "SendHelperResponse",
Handler: _StartedService_SendHelperResponse_Handler, Handler: _StartedService_SendHelperResponse_Handler,

View File

@@ -95,6 +95,20 @@ func (c *Client) Start() {
} }
} }
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
for _, record := range response.Ns {
if soa, isSOA := record.(*dns.SOA); isSOA {
soaTTL := soa.Header().Ttl
soaMinimum := soa.Minttl
if soaTTL < soaMinimum {
return soaTTL, true
}
return soaMinimum, true
}
}
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) { func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
if len(message.Question) == 0 { if len(message.Question) == 0 {
if c.logger != nil { if c.logger != nil {
@@ -214,7 +228,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response.Answer = append(response.Answer, validResponse.Answer...) response.Answer = append(response.Answer, validResponse.Answer...)
} }
}*/ }*/
disableCache = disableCache || response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0 disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil { if responseChecker != nil {
var rejected bool var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses // TODO: add accept_any rule and support to check response instead of addresses
@@ -251,10 +265,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
} }
var timeToLive uint32 var timeToLive uint32
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { if len(response.Answer) == 0 {
for _, record := range recordList { if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { timeToLive = soaTTL
timeToLive = record.Header().Ttl }
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
} }
} }
} }
@@ -332,64 +353,6 @@ func (c *Client) ClearCache() {
} }
} }
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
if c.disableCache || c.independentCache {
return nil, false
}
if dns.IsFqdn(domain) {
domain = domain[:len(domain)-1]
}
dnsName := dns.Fqdn(domain)
if strategy == C.DomainStrategyIPv4Only {
addresses, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return addresses, true
}
} else if strategy == C.DomainStrategyIPv6Only {
addresses, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return addresses, true
}
} else {
response4, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
response6, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if response4 != nil || response6 != nil {
return sortAddresses(MessageToAddresses(response4), MessageToAddresses(response6), strategy), true
}
}
return nil, false
}
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
if c.disableCache || c.independentCache || len(message.Question) != 1 {
return nil, false
}
question := message.Question[0]
response, ttl := c.loadResponse(question, nil)
if response == nil {
return nil, false
}
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, true
}
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr { func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
if strategy == C.DomainStrategyPreferIPv6 { if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...) return append(response6, response4...)

View File

@@ -213,97 +213,95 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} }
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String())) r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var ( var (
response *mDNS.Msg
transport adapter.DNSTransport transport adapter.DNSTransport
err error err error
) )
response, cached := r.client.ExchangeCache(ctx, message) var metadata *adapter.InboundContext
if !cached { ctx, metadata = adapter.ExtendContext(ctx)
var metadata *adapter.InboundContext metadata.Destination = M.Socksaddr{}
ctx, metadata = adapter.ExtendContext(ctx) metadata.QueryType = message.Question[0].Qtype
metadata.Destination = M.Socksaddr{} switch metadata.QueryType {
metadata.QueryType = message.Question[0].Qtype case mDNS.TypeA:
switch metadata.QueryType { metadata.IPVersion = 4
case mDNS.TypeA: case mDNS.TypeAAAA:
metadata.IPVersion = 4 metadata.IPVersion = 6
case mDNS.TypeAAAA: }
metadata.IPVersion = 6 metadata.Domain = FqdnToDomain(message.Question[0].Name)
} if options.Transport != nil {
metadata.Domain = FqdnToDomain(message.Question[0].Name) transport = options.Transport
if options.Transport != nil { if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS { if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy options.Strategy = legacyTransport.LegacyStrategy()
} }
response, err = r.client.Exchange(ctx, transport, message, options, nil) if !options.ClientSubnet.IsValid() {
} else { options.ClientSubnet = legacyTransport.LegacyClientSubnet()
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
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
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
} }
} }
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
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
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
}
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -326,7 +324,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var ( var (
responseAddrs []netip.Addr responseAddrs []netip.Addr
cached bool
err error err error
) )
printResult := func() { printResult := func() {
@@ -346,13 +343,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err = E.Cause(err, "lookup ", domain) err = E.Cause(err, "lookup ", domain)
} }
} }
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}
r.logger.DebugContext(ctx, "lookup domain ", domain) r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx) ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{} metadata.Destination = M.Socksaddr{}
@@ -385,12 +375,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
if rule != nil { if rule != nil {
switch action := rule.Action().(type) { switch action := rule.Action().(type) {
case *R.RuleActionReject: case *R.RuleActionReject:
switch action.Method { return nil, &R.RejectedError{Cause: action.Error(ctx)}
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined: case *R.RuleActionPredefined:
if action.Rcode != mDNS.RcodeSuccess { if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode) err = RcodeError(action.Rcode)
@@ -459,6 +444,6 @@ func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
func (r *Router) ResetNetwork() { func (r *Router) ResetNetwork() {
r.ClearCache() r.ClearCache()
for _, transport := range r.transport.Transports() { for _, transport := range r.transport.Transports() {
transport.Close() transport.Reset()
} }
} }

145
dns/transport/base.go Normal file
View File

@@ -0,0 +1,145 @@
package transport
import (
"context"
"os"
"sync"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type TransportState int
const (
StateNew TransportState = iota
StateStarted
StateClosing
StateClosed
)
var (
ErrTransportClosed = os.ErrClosed
ErrConnectionReset = E.New("connection reset")
)
type BaseTransport struct {
dns.TransportAdapter
Logger logger.ContextLogger
mutex sync.Mutex
state TransportState
inFlight int32
queriesComplete chan struct{}
closeCtx context.Context
closeCancel context.CancelFunc
}
func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport {
ctx, cancel := context.WithCancel(context.Background())
return &BaseTransport{
TransportAdapter: adapter,
Logger: logger,
state: StateNew,
closeCtx: ctx,
closeCancel: cancel,
}
}
func (t *BaseTransport) State() TransportState {
t.mutex.Lock()
defer t.mutex.Unlock()
return t.state
}
func (t *BaseTransport) SetStarted() error {
t.mutex.Lock()
defer t.mutex.Unlock()
switch t.state {
case StateNew:
t.state = StateStarted
return nil
case StateStarted:
return nil
default:
return ErrTransportClosed
}
}
func (t *BaseTransport) BeginQuery() bool {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.state != StateStarted {
return false
}
t.inFlight++
return true
}
func (t *BaseTransport) EndQuery() {
t.mutex.Lock()
if t.inFlight > 0 {
t.inFlight--
}
if t.inFlight == 0 && t.queriesComplete != nil {
close(t.queriesComplete)
t.queriesComplete = nil
}
t.mutex.Unlock()
}
func (t *BaseTransport) CloseContext() context.Context {
return t.closeCtx
}
func (t *BaseTransport) Shutdown(ctx context.Context) error {
t.mutex.Lock()
if t.state >= StateClosing {
t.mutex.Unlock()
return nil
}
if t.state == StateNew {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.state = StateClosing
if t.inFlight == 0 {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.queriesComplete = make(chan struct{})
queriesComplete := t.queriesComplete
t.mutex.Unlock()
t.closeCancel()
select {
case <-queriesComplete:
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return nil
case <-ctx.Done():
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return ctx.Err()
}
}
func (t *BaseTransport) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
defer cancel()
return t.Shutdown(ctx)
}

205
dns/transport/connector.go Normal file
View File

@@ -0,0 +1,205 @@
package transport
import (
"context"
"net"
"sync"
)
type ConnectorCallbacks[T any] struct {
IsClosed func(connection T) bool
Close func(connection T)
Reset func(connection T)
}
type Connector[T any] struct {
dial func(ctx context.Context) (T, error)
callbacks ConnectorCallbacks[T]
access sync.Mutex
connection T
hasConnection bool
connecting chan struct{}
closeCtx context.Context
closed bool
}
func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] {
return &Connector[T]{
dial: dial,
callbacks: callbacks,
closeCtx: closeCtx,
}
}
func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] {
return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{
IsClosed: func(connection *Connection) bool {
return connection.IsClosed()
},
Close: func(connection *Connection) {
connection.CloseWithError(ErrTransportClosed)
},
Reset: func(connection *Connection) {
connection.CloseWithError(ErrConnectionReset)
},
})
}
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
c.access.Lock()
if c.closed {
c.access.Unlock()
return zero, ErrTransportClosed
}
if c.hasConnection && !c.callbacks.IsClosed(c.connection) {
connection := c.connection
c.access.Unlock()
return connection, nil
}
c.hasConnection = false
if c.connecting != nil {
connecting := c.connecting
c.access.Unlock()
select {
case <-connecting:
continue
case <-ctx.Done():
return zero, ctx.Err()
case <-c.closeCtx.Done():
return zero, ErrTransportClosed
}
}
c.connecting = make(chan struct{})
c.access.Unlock()
connection, err := c.dialWithCancellation(ctx)
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
c.callbacks.Close(connection)
c.access.Unlock()
return zero, ErrTransportClosed
}
c.connection = connection
c.hasConnection = true
result := c.connection
c.access.Unlock()
return result, nil
}
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, error) {
dialCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
select {
case <-c.closeCtx.Done():
cancel()
case <-dialCtx.Done():
}
}()
return c.dial(dialCtx)
}
func (c *Connector[T]) Close() error {
c.access.Lock()
defer c.access.Unlock()
if c.closed {
return nil
}
c.closed = true
if c.hasConnection {
c.callbacks.Close(c.connection)
c.hasConnection = false
}
return nil
}
func (c *Connector[T]) Reset() {
c.access.Lock()
defer c.access.Unlock()
if c.hasConnection {
c.callbacks.Reset(c.connection)
c.hasConnection = false
}
}
type Connection struct {
net.Conn
closeOnce sync.Once
done chan struct{}
closeError error
}
func WrapConnection(conn net.Conn) *Connection {
return &Connection{
Conn: conn,
done: make(chan struct{}),
}
}
func (c *Connection) Done() <-chan struct{} {
return c.done
}
func (c *Connection) IsClosed() bool {
select {
case <-c.done:
return true
default:
return false
}
}
func (c *Connection) CloseError() error {
select {
case <-c.done:
if c.closeError != nil {
return c.closeError
}
return ErrTransportClosed
default:
return nil
}
}
func (c *Connection) Close() error {
return c.CloseWithError(ErrTransportClosed)
}
func (c *Connection) CloseWithError(err error) error {
var returnError error
c.closeOnce.Do(func() {
c.closeError = err
returnError = c.Conn.Close()
close(c.done)
})
return returnError
}

View File

@@ -49,6 +49,7 @@ type Transport struct {
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transportLock sync.RWMutex transportLock sync.RWMutex
updatedAt time.Time updatedAt time.Time
lastError error
servers []M.Socksaddr servers []M.Socksaddr
search []string search []string
ndots int ndots int
@@ -92,7 +93,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
} }
go func() { go func() {
_, err := t.Fetch() _, err := t.fetch()
if err != nil { if err != nil {
t.logger.Error(E.Cause(err, "fetch DNS servers")) t.logger.Error(E.Cause(err, "fetch DNS servers"))
} }
@@ -107,8 +108,15 @@ func (t *Transport) Close() error {
return nil return nil
} }
func (t *Transport) Reset() {
t.transportLock.Lock()
t.updatedAt = time.Time{}
t.servers = nil
t.transportLock.Unlock()
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
servers, err := t.Fetch() servers, err := t.fetch()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -128,11 +136,20 @@ func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []
} }
} }
func (t *Transport) Fetch() ([]M.Socksaddr, error) { func (t *Transport) Fetch() []M.Socksaddr {
servers, _ := t.fetch()
return servers
}
func (t *Transport) fetch() ([]M.Socksaddr, error) {
t.transportLock.RLock() t.transportLock.RLock()
updatedAt := t.updatedAt updatedAt := t.updatedAt
lastError := t.lastError
servers := t.servers servers := t.servers
t.transportLock.RUnlock() t.transportLock.RUnlock()
if lastError != nil {
return nil, lastError
}
if time.Since(updatedAt) < C.DHCPTTL { if time.Since(updatedAt) < C.DHCPTTL {
return servers, nil return servers, nil
} }
@@ -143,7 +160,7 @@ func (t *Transport) Fetch() ([]M.Socksaddr, error) {
} }
err := t.updateServers() err := t.updateServers()
if err != nil { if err != nil {
return nil, err return servers, err
} }
return t.servers, nil return t.servers, nil
} }
@@ -173,12 +190,15 @@ func (t *Transport) updateServers() error {
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout) fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
err = t.fetchServers0(fetchCtx, iface) err = t.fetchServers0(fetchCtx, iface)
cancel() cancel()
t.updatedAt = time.Now()
if err != nil { if err != nil {
t.lastError = err
return err return err
} else if len(t.servers) == 0 { } else if len(t.servers) == 0 {
return E.New("dhcp: empty DNS servers response") t.lastError = E.New("dhcp: empty DNS servers response")
return t.lastError
} else { } else {
t.updatedAt = time.Now() t.lastError = nil
return nil return nil
} }
} }

View File

@@ -82,8 +82,12 @@ func (s *MemoryStorage) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr
} }
func (s *MemoryStorage) FakeIPReset() error { func (s *MemoryStorage) FakeIPReset() error {
s.addressAccess.Lock()
s.domainAccess.Lock()
s.addressCache = make(map[netip.Addr]string) s.addressCache = make(map[netip.Addr]string)
s.domainCache4 = make(map[string]netip.Addr) s.domainCache4 = make(map[string]netip.Addr)
s.domainCache6 = make(map[string]netip.Addr) s.domainCache6 = make(map[string]netip.Addr)
s.domainAccess.Unlock()
s.addressAccess.Unlock()
return nil return nil
} }

View File

@@ -3,6 +3,7 @@ package fakeip
import ( import (
"context" "context"
"net/netip" "net/netip"
"sync"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -13,13 +14,15 @@ import (
var _ adapter.FakeIPStore = (*Store)(nil) var _ adapter.FakeIPStore = (*Store)(nil)
type Store struct { type Store struct {
ctx context.Context ctx context.Context
logger logger.Logger logger logger.Logger
inet4Range netip.Prefix inet4Range netip.Prefix
inet6Range netip.Prefix inet6Range netip.Prefix
storage adapter.FakeIPStorage storage adapter.FakeIPStorage
inet4Current netip.Addr
inet6Current netip.Addr addressAccess sync.Mutex
inet4Current netip.Addr
inet6Current netip.Addr
} }
func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
@@ -65,18 +68,30 @@ func (s *Store) Close() error {
if s.storage == nil { if s.storage == nil {
return nil return nil
} }
return s.storage.FakeIPSaveMetadata(&adapter.FakeIPMetadata{ s.addressAccess.Lock()
metadata := &adapter.FakeIPMetadata{
Inet4Range: s.inet4Range, Inet4Range: s.inet4Range,
Inet6Range: s.inet6Range, Inet6Range: s.inet6Range,
Inet4Current: s.inet4Current, Inet4Current: s.inet4Current,
Inet6Current: s.inet6Current, Inet6Current: s.inet6Current,
}) }
s.addressAccess.Unlock()
return s.storage.FakeIPSaveMetadata(metadata)
} }
func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) { func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded { if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded {
return address, nil return address, nil
} }
s.addressAccess.Lock()
defer s.addressAccess.Unlock()
// Double-check after acquiring lock
if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded {
return address, nil
}
var address netip.Addr var address netip.Addr
if !isIPv6 { if !isIPv6 {
if !s.inet4Current.IsValid() { if !s.inet4Current.IsValid() {
@@ -99,7 +114,10 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
s.inet6Current = nextAddress s.inet6Current = nextAddress
address = nextAddress address = nextAddress
} }
s.storage.FakeIPStoreAsync(address, domain, s.logger) err := s.storage.FakeIPStore(address, domain)
if err != nil {
s.logger.Warn("save FakeIP cache: ", err)
}
s.storage.FakeIPSaveMetadataAsync(&adapter.FakeIPMetadata{ s.storage.FakeIPSaveMetadataAsync(&adapter.FakeIPMetadata{
Inet4Range: s.inet4Range, Inet4Range: s.inet4Range,
Inet6Range: s.inet6Range, Inet6Range: s.inet6Range,

View File

@@ -59,6 +59,9 @@ func (t *Transport) Close() error {
return nil return nil
} }
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0] question := message.Question[0]
domain := mDNS.CanonicalName(question.Name) domain := mDNS.CanonicalName(question.Name)

View File

@@ -145,6 +145,13 @@ func (t *HTTPSTransport) Close() error {
return nil return nil
} }
func (t *HTTPSTransport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
}
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
startAt := time.Now() startAt := time.Now()
response, err := t.exchange(ctx, message) response, err := t.exchange(ctx, message)
@@ -182,7 +189,10 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
request.Header = t.headers.Clone() request.Header = t.headers.Clone()
request.Header.Set("Content-Type", MimeType) request.Header.Set("Content-Type", MimeType)
request.Header.Set("Accept", MimeType) request.Header.Set("Accept", MimeType)
response, err := t.transport.RoundTrip(request) t.transportAccess.Lock()
currentTransport := t.transport
t.transportAccess.Unlock()
response, err := currentTransport.RoundTrip(request)
requestBuffer.Release() requestBuffer.Release()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -194,12 +204,12 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
var responseMessage mDNS.Msg var responseMessage mDNS.Msg
if response.ContentLength > 0 { if response.ContentLength > 0 {
responseBuffer := buf.NewSize(int(response.ContentLength)) responseBuffer := buf.NewSize(int(response.ContentLength))
defer responseBuffer.Release()
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength)) _, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = responseMessage.Unpack(responseBuffer.Bytes()) err = responseMessage.Unpack(responseBuffer.Bytes())
responseBuffer.Release()
} else { } else {
rawMessage, err = io.ReadAll(response.Body) rawMessage, err = io.ReadAll(response.Body)
if err != nil { if err != nil {

View File

@@ -75,5 +75,6 @@ func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
http2Transport: &http2.Transport{ http2Transport: &http2.Transport{
DialTLSContext: h.http2Transport.DialTLSContext, DialTLSContext: h.http2Transport.DialTLSContext,
}, },
fallback: h.fallback,
} }
} }

View File

@@ -53,13 +53,15 @@ func (t *Transport) Start(stage adapter.StartStage) error {
switch stage { switch stage {
case adapter.StartStateInitialize: case adapter.StartStateInitialize:
if !t.preferGo { if !t.preferGo {
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) if isSystemdResolvedManaged() {
if err == nil { resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
err = resolvedResolver.Start()
if err == nil { if err == nil {
t.resolved = resolvedResolver err = resolvedResolver.Start()
} else { if err == nil {
t.logger.Warn(E.Cause(err, "initialize resolved resolver")) t.resolved = resolvedResolver
} else {
t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
}
} }
} }
} }
@@ -74,6 +76,9 @@ func (t *Transport) Close() error {
return nil return nil
} }
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil { if t.resolved != nil {
resolverObject := t.resolved.Object() resolverObject := t.resolved.Object()
@@ -82,12 +87,11 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} }
} }
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(domain) addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 { if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} }
} }
return t.exchange(ctx, message, domain) return t.exchange(ctx, message, question.Name)
} }

View File

@@ -43,7 +43,7 @@ type Transport struct {
type dhcpTransport interface { type dhcpTransport interface {
adapter.DNSTransport adapter.DNSTransport
Fetch() ([]M.Socksaddr, error) Fetch() []M.Socksaddr
Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error)
} }
@@ -74,14 +74,12 @@ func (t *Transport) Start(stage adapter.StartStage) error {
break break
} }
} }
if !C.IsIos { if t.fallback {
if t.fallback { t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) if t.dhcpTransport != nil {
if t.dhcpTransport != nil { err := t.dhcpTransport.Start(stage)
err := t.dhcpTransport.Start(stage) if err != nil {
if err != nil { return err
return err
}
} }
} }
} }
@@ -94,29 +92,32 @@ func (t *Transport) Close() error {
) )
} }
func (t *Transport) Reset() {
if t.dhcpTransport != nil {
t.dhcpTransport.Reset()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(domain) addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 { if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} }
} }
if !t.fallback { if !t.fallback {
return t.exchange(ctx, message, domain) return t.exchange(ctx, message, question.Name)
} }
if !C.IsIos { if t.dhcpTransport != nil {
if t.dhcpTransport != nil { dhcpTransports := t.dhcpTransport.Fetch()
dhcpTransports, _ := t.dhcpTransport.Fetch() if len(dhcpTransports) > 0 {
if len(dhcpTransports) > 0 { return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
} }
} }
if t.preferGo { if t.preferGo {
// Assuming the user knows what they are doing, we still execute the query which will fail. // Assuming the user knows what they are doing, we still execute the query which will fail.
return t.exchange(ctx, message, domain) return t.exchange(ctx, message, question.Name)
} }
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string var network string
@@ -125,7 +126,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} else { } else {
network = "ip6" network = "ip6"
} }
addresses, err := t.resolver.LookupNetIP(ctx, network, domain) addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -135,9 +136,5 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} }
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} }
if C.IsIos { return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
return nil, E.New("only A and AAAA queries are supported on iOS and tvOS when using NetworkExtension.")
} else {
return nil, E.New("only A and AAAA queries are supported on macOS when using NetworkExtension and DHCP unavailable.")
}
} }

View File

@@ -1,9 +1,11 @@
package local package local
import ( import (
"bufio"
"context" "context"
"errors" "errors"
"os" "os"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -22,6 +24,25 @@ import (
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
func isSystemdResolvedManaged() bool {
resolvContent, err := os.Open("/etc/resolv.conf")
if err != nil {
return false
}
defer resolvContent.Close()
scanner := bufio.NewScanner(resolvContent)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || line[0] != '#' {
return false
}
if strings.Contains(line, "systemd-resolved") {
return true
}
}
return false
}
type DBusResolvedResolver struct { type DBusResolvedResolver struct {
ctx context.Context ctx context.Context
logger logger.ContextLogger logger logger.ContextLogger
@@ -188,7 +209,7 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
int32(defaultInterface.Index), int32(defaultInterface.Index),
) )
if call.Err != nil { if call.Err != nil {
return nil, err return nil, call.Err
} }
var linkPath dbus.ObjectPath var linkPath dbus.ObjectPath
err = call.Store(&linkPath) err = call.Store(&linkPath)
@@ -214,15 +235,12 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
return nil, E.New("No appropriate name servers or networks for name found") return nil, E.New("No appropriate name servers or networks for name found")
} }
} }
return &ResolvedObject{ return nil, E.New("link has no DNS servers configured")
BusObject: dbusObject,
}, nil
} else {
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, nil
} }
return &ResolvedObject{
BusObject: dbusObject,
InterfaceIndex: int32(defaultInterface.Index),
}, nil
} }
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {

View File

@@ -9,6 +9,10 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
func isSystemdResolvedManaged() bool {
return false
}
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }

View File

@@ -8,10 +8,12 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"sync"
"github.com/sagernet/quic-go" "github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3" "github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
@@ -23,6 +25,7 @@ import (
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
sHTTP "github.com/sagernet/sing/protocol/http" sHTTP "github.com/sagernet/sing/protocol/http"
@@ -37,11 +40,14 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
type HTTP3Transport struct { type HTTP3Transport struct {
dns.TransportAdapter dns.TransportAdapter
logger logger.ContextLogger logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
destination *url.URL destination *url.URL
headers http.Header headers http.Header
transport *http3.Transport serverAddr M.Socksaddr
tlsConfig *tls.STDConfig
transportAccess sync.Mutex
transport *http3.Transport
} }
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -95,33 +101,57 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
if !serverAddr.IsValid() { if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr) return nil, E.New("invalid server address: ", serverAddr)
} }
return &HTTP3Transport{ t := &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger, logger: logger,
dialer: transportDialer, dialer: transportDialer,
destination: &destinationURL, destination: &destinationURL,
headers: headers, headers: headers,
transport: &http3.Transport{ serverAddr: serverAddr,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) { tlsConfig: stdConfig,
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, serverAddr) }
if dialErr != nil { t.transport = t.newTransport()
return nil, dialErr return t, nil
} }
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
}, func (t *HTTP3Transport) newTransport() *http3.Transport {
TLSClientConfig: stdConfig, return &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if dialErr != nil {
return nil, dialErr
}
quicConn, dialErr := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
if dialErr != nil {
conn.Close()
return nil, dialErr
}
return quicConn, nil
}, },
}, nil TLSClientConfig: t.tlsConfig,
}
} }
func (t *HTTP3Transport) Start(stage adapter.StartStage) error { func (t *HTTP3Transport) Start(stage adapter.StartStage) error {
return nil if stage != adapter.StartStateStart {
return nil
}
return dialer.InitializeDetour(t.dialer)
} }
func (t *HTTP3Transport) Close() error { func (t *HTTP3Transport) Close() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
return t.transport.Close() return t.transport.Close()
} }
func (t *HTTP3Transport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.Close()
t.transport = t.newTransport()
}
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
exMessage := *message exMessage := *message
exMessage.Id = 0 exMessage.Id = 0
@@ -140,7 +170,10 @@ func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
request.Header = t.headers.Clone() request.Header = t.headers.Clone()
request.Header.Set("Content-Type", transport.MimeType) request.Header.Set("Content-Type", transport.MimeType)
request.Header.Set("Accept", transport.MimeType) request.Header.Set("Accept", transport.MimeType)
response, err := t.transport.RoundTrip(request) t.transportAccess.Lock()
currentTransport := t.transport
t.transportAccess.Unlock()
response, err := currentTransport.RoundTrip(request)
requestBuffer.Release() requestBuffer.Release()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -152,12 +185,12 @@ func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
var responseMessage mDNS.Msg var responseMessage mDNS.Msg
if response.ContentLength > 0 { if response.ContentLength > 0 {
responseBuffer := buf.NewSize(int(response.ContentLength)) responseBuffer := buf.NewSize(int(response.ContentLength))
defer responseBuffer.Release()
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength)) _, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = responseMessage.Unpack(responseBuffer.Bytes()) err = responseMessage.Unpack(responseBuffer.Bytes())
responseBuffer.Release()
} else { } else {
rawMessage, err = io.ReadAll(response.Body) rawMessage, err = io.ReadAll(response.Body)
if err != nil { if err != nil {

View File

@@ -3,10 +3,11 @@ package quic
import ( import (
"context" "context"
"errors" "errors"
"sync" "os"
"github.com/sagernet/quic-go" "github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
@@ -17,7 +18,6 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -31,14 +31,14 @@ func RegisterTransport(registry *dns.TransportRegistry) {
} }
type Transport struct { type Transport struct {
dns.TransportAdapter *transport.BaseTransport
ctx context.Context ctx context.Context
logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
access sync.Mutex
connection *quic.Conn connector *transport.Connector[*quic.Conn]
} }
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -62,38 +62,84 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
if !serverAddr.IsValid() { if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr) return nil, E.New("invalid server address: ", serverAddr)
} }
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), t := &Transport{
ctx: ctx, BaseTransport: transport.NewBaseTransport(
logger: logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
dialer: transportDialer, logger,
serverAddr: serverAddr, ),
tlsConfig: tlsConfig, ctx: ctx,
}, nil dialer: transportDialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}
t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{
IsClosed: func(connection *quic.Conn) bool {
return common.Done(connection.Context())
},
Close: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
Reset: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
})
return t, nil
}
func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
earlyConnection, err := sQUIC.DialEarly(
ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
conn.Close()
return nil, E.Cause(err, "establish QUIC connection")
}
return earlyConnection, nil
} }
func (t *Transport) Start(stage adapter.StartStage) error { func (t *Transport) Start(stage adapter.StartStage) error {
return nil if stage != adapter.StartStateStart {
return nil
}
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer)
} }
func (t *Transport) Close() error { func (t *Transport) Close() error {
t.access.Lock() return E.Errors(t.BaseTransport.Close(), t.connector.Close())
defer t.access.Unlock() }
connection := t.connection
if connection != nil { func (t *Transport) Reset() {
connection.CloseWithError(0, "") t.connector.Reset()
}
return nil
} }
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, transport.ErrTransportClosed
}
defer t.EndQuery()
var ( var (
conn *quic.Conn conn *quic.Conn
err error err error
response *mDNS.Msg response *mDNS.Msg
) )
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
conn, err = t.openConnection() conn, err = t.connector.Get(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -103,58 +149,38 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
} else if !isQUICRetryError(err) { } else if !isQUICRetryError(err) {
return nil, err return nil, err
} else { } else {
conn.CloseWithError(quic.ApplicationErrorCode(0), "") t.connector.Reset()
continue continue
} }
} }
return nil, err return nil, err
} }
func (t *Transport) openConnection() (*quic.Conn, error) {
connection := t.connection
if connection != nil && !common.Done(connection.Context()) {
return connection, nil
}
t.access.Lock()
defer t.access.Unlock()
connection = t.connection
if connection != nil && !common.Done(connection.Context()) {
return connection, nil
}
conn, err := t.dialer.DialContext(t.ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
}
earlyConnection, err := sQUIC.DialEarly(
t.ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
return nil, err
}
t.connection = earlyConnection
return earlyConnection, nil
}
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn *quic.Conn) (*mDNS.Msg, error) { func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn *quic.Conn) (*mDNS.Msg, error) {
stream, err := conn.OpenStreamSync(ctx) stream, err := conn.OpenStreamSync(ctx)
if err != nil { if err != nil {
return nil, err return nil, E.Cause(err, "open stream")
} }
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message) err = transport.WriteMessage(stream, 0, message)
if err != nil { if err != nil {
stream.Close() stream.Close()
return nil, err return nil, E.Cause(err, "write request")
} }
stream.Close() stream.Close()
return transport.ReadMessage(stream) response, err := transport.ReadMessage(stream)
if err != nil {
return nil, E.Cause(err, "read response")
}
return response, nil
} }
// https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394 // https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394
func isQUICRetryError(err error) (ok bool) { func isQUICRetryError(err error) (ok bool) {
if errors.Is(err, os.ErrClosed) {
return true
}
var qAppErr *quic.ApplicationError var qAppErr *quic.ApplicationError
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 {
return true return true

View File

@@ -62,17 +62,24 @@ func (t *TCPTransport) Close() error {
return nil return nil
} }
func (t *TCPTransport) Reset() {
}
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr) conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
if err != nil { if err != nil {
return nil, err return nil, E.Cause(err, "dial TCP connection")
} }
defer conn.Close() defer conn.Close()
err = WriteMessage(conn, 0, message) err = WriteMessage(conn, 0, message)
if err != nil { if err != nil {
return nil, err return nil, E.Cause(err, "write request")
} }
return ReadMessage(conn) response, err := ReadMessage(conn)
if err != nil {
return nil, E.Cause(err, "read response")
}
return response, nil
} }
func ReadMessage(reader io.Reader) (*mDNS.Msg, error) { func ReadMessage(reader io.Reader) (*mDNS.Msg, error) {

View File

@@ -3,6 +3,7 @@ package transport
import ( import (
"context" "context"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
@@ -28,8 +29,8 @@ func RegisterTLS(registry *dns.TransportRegistry) {
} }
type TLSTransport struct { type TLSTransport struct {
dns.TransportAdapter *BaseTransport
logger logger.ContextLogger
dialer tls.Dialer dialer tls.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
@@ -65,11 +66,10 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport { func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{ return &TLSTransport{
TransportAdapter: adapter, BaseTransport: NewBaseTransport(adapter, logger),
logger: logger, dialer: tls.NewDialer(dialer, tlsConfig),
dialer: tls.NewDialer(dialer, tlsConfig), serverAddr: serverAddr,
serverAddr: serverAddr, tlsConfig: tlsConfig,
tlsConfig: tlsConfig,
} }
} }
@@ -77,37 +77,59 @@ func (t *TLSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *TLSTransport) Close() error { func (t *TLSTransport) Close() error {
t.access.Lock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
t.access.Unlock()
return t.BaseTransport.Close()
}
func (t *TLSTransport) Reset() {
t.access.Lock() t.access.Lock()
defer t.access.Unlock() defer t.access.Unlock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() { for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close() connection.Value.Close()
} }
t.connections.Init() t.connections.Init()
return nil
} }
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, ErrTransportClosed
}
defer t.EndQuery()
t.access.Lock() t.access.Lock()
conn := t.connections.PopFront() conn := t.connections.PopFront()
t.access.Unlock() t.access.Unlock()
if conn != nil { if conn != nil {
response, err := t.exchange(message, conn) response, err := t.exchange(ctx, message, conn)
if err == nil { if err == nil {
return response, nil return response, nil
} }
t.Logger.DebugContext(ctx, "discarded pooled connection: ", err)
} }
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
if err != nil { if err != nil {
return nil, err return nil, E.Cause(err, "dial TLS connection")
} }
return t.exchange(message, &tlsDNSConn{Conn: tlsConn}) return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
} }
func (t *TLSTransport) exchange(message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
if deadline, ok := ctx.Deadline(); ok {
conn.SetDeadline(deadline)
}
conn.queryId++ conn.queryId++
err := WriteMessage(conn, conn.queryId, message) err := WriteMessage(conn, conn.queryId, message)
if err != nil { if err != nil {
@@ -120,6 +142,12 @@ func (t *TLSTransport) exchange(message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg,
return nil, E.Cause(err, "read response") return nil, E.Cause(err, "read response")
} }
t.access.Lock() t.access.Lock()
if t.State() >= StateClosing {
t.access.Unlock()
conn.Close()
return response, nil
}
conn.SetDeadline(time.Time{})
t.connections.PushBack(conn) t.connections.PushBack(conn)
t.access.Unlock() t.access.Unlock()
return response, nil return response, nil

View File

@@ -2,9 +2,8 @@ package transport
import ( import (
"context" "context"
"net"
"os"
"sync" "sync"
"sync/atomic"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
@@ -28,15 +27,23 @@ func RegisterUDP(registry *dns.TransportRegistry) {
} }
type UDPTransport struct { type UDPTransport struct {
dns.TransportAdapter *BaseTransport
logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
udpSize int udpSize atomic.Int32
tcpTransport *TCPTransport
access sync.Mutex connector *Connector[*Connection]
conn *dnsConnection
done chan struct{} callbackAccess sync.RWMutex
queryId uint16
callbacks map[uint16]*udpCallback
}
type udpCallback struct {
access sync.Mutex
response *mDNS.Msg
done chan struct{}
} }
func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) { func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
@@ -54,180 +61,198 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
} }
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr) *UDPTransport { func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
return &UDPTransport{ t := &UDPTransport{
TransportAdapter: adapter, BaseTransport: NewBaseTransport(adapter, logger),
logger: logger, dialer: dialerInstance,
dialer: dialer, serverAddr: serverAddr,
serverAddr: serverAddr, callbacks: make(map[uint16]*udpCallback),
udpSize: 2048,
tcpTransport: &TCPTransport{
dialer: dialer,
serverAddr: serverAddr,
},
done: make(chan struct{}),
} }
t.udpSize.Store(2048)
t.connector = NewSingleflightConnector(t.CloseContext(), t.dial)
return t
}
func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) {
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
conn := WrapConnection(rawConn)
go t.recvLoop(conn)
return conn, nil
} }
func (t *UDPTransport) Start(stage adapter.StartStage) error { func (t *UDPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *UDPTransport) Close() error { func (t *UDPTransport) Close() error {
t.access.Lock() return E.Errors(t.BaseTransport.Close(), t.connector.Close())
defer t.access.Unlock() }
close(t.done)
t.done = make(chan struct{}) func (t *UDPTransport) Reset() {
return nil t.connector.Reset()
}
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
start := t.queryId
for {
t.queryId++
if _, exists := t.callbacks[t.queryId]; !exists {
return t.queryId, nil
}
if t.queryId == start {
return 0, E.New("no available query ID")
}
}
} }
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, ErrTransportClosed
}
defer t.EndQuery()
response, err := t.exchange(ctx, message) response, err := t.exchange(ctx, message)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.Truncated { if response.Truncated {
t.logger.InfoContext(ctx, "response truncated, retrying with TCP") t.Logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.tcpTransport.Exchange(ctx, message) return t.exchangeTCP(ctx, message)
}
return response, nil
}
func (t *UDPTransport) exchangeTCP(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial TCP connection")
}
defer conn.Close()
err = WriteMessage(conn, message.Id, message)
if err != nil {
return nil, E.Cause(err, "write request")
}
response, err := ReadMessage(conn)
if err != nil {
return nil, E.Cause(err, "read response")
} }
return response, nil return response, nil
} }
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
t.access.Lock()
if edns0Opt := message.IsEdns0(); edns0Opt != nil { if edns0Opt := message.IsEdns0(); edns0Opt != nil {
if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize { udpSize := int32(edns0Opt.UDPSize())
t.udpSize = udpSize for {
close(t.done) current := t.udpSize.Load()
t.done = make(chan struct{}) if udpSize <= current {
break
}
if t.udpSize.CompareAndSwap(current, udpSize) {
t.connector.Reset()
break
}
} }
} }
t.access.Unlock()
conn, err := t.open(ctx) conn, err := t.connector.Get(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
buffer := buf.NewSize(1 + message.Len())
defer buffer.Release() callback := &udpCallback{
exMessage := *message
exMessage.Compress = true
messageId := message.Id
callback := &dnsCallback{
done: make(chan struct{}), done: make(chan struct{}),
} }
conn.access.Lock()
conn.queryId++ t.callbackAccess.Lock()
exMessage.Id = conn.queryId queryId, err := t.nextAvailableQueryId()
conn.callbacks[exMessage.Id] = callback if err != nil {
conn.access.Unlock() t.callbackAccess.Unlock()
return nil, err
}
t.callbacks[queryId] = callback
t.callbackAccess.Unlock()
defer func() { defer func() {
conn.access.Lock() t.callbackAccess.Lock()
delete(conn.callbacks, exMessage.Id) delete(t.callbacks, queryId)
conn.access.Unlock() t.callbackAccess.Unlock()
}() }()
buffer := buf.NewSize(1 + message.Len())
defer buffer.Release()
exMessage := *message
exMessage.Compress = true
originalId := message.Id
exMessage.Id = queryId
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes()) rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = conn.Write(rawMessage) _, err = conn.Write(rawMessage)
if err != nil { if err != nil {
conn.Close(err) conn.CloseWithError(err)
return nil, err return nil, E.Cause(err, "write request")
} }
select { select {
case <-callback.done: case <-callback.done:
callback.message.Id = messageId callback.response.Id = originalId
return callback.message, nil return callback.response, nil
case <-conn.done: case <-conn.Done():
return nil, conn.err return nil, conn.CloseError()
case <-t.done: case <-t.CloseContext().Done():
return nil, os.ErrClosed return nil, ErrTransportClosed
case <-ctx.Done(): case <-ctx.Done():
conn.Close(ctx.Err())
return nil, ctx.Err() return nil, ctx.Err()
} }
} }
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) { func (t *UDPTransport) recvLoop(conn *Connection) {
t.access.Lock()
defer t.access.Unlock()
if t.conn != nil {
select {
case <-t.conn.done:
default:
return t.conn, nil
}
}
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
}
dnsConn := &dnsConnection{
Conn: conn,
done: make(chan struct{}),
callbacks: make(map[uint16]*dnsCallback),
}
go t.recvLoop(dnsConn)
t.conn = dnsConn
return dnsConn, nil
}
func (t *UDPTransport) recvLoop(conn *dnsConnection) {
for { for {
buffer := buf.NewSize(t.udpSize) buffer := buf.NewSize(int(t.udpSize.Load()))
_, err := buffer.ReadOnceFrom(conn) _, err := buffer.ReadOnceFrom(conn)
if err != nil { if err != nil {
buffer.Release() buffer.Release()
conn.Close(err) conn.CloseWithError(err)
return return
} }
var message mDNS.Msg var message mDNS.Msg
err = message.Unpack(buffer.Bytes()) err = message.Unpack(buffer.Bytes())
buffer.Release() buffer.Release()
if err != nil { if err != nil {
conn.Close(err) t.Logger.Debug("discarded malformed UDP response: ", err)
return continue
} }
conn.access.RLock()
callback, loaded := conn.callbacks[message.Id] t.callbackAccess.RLock()
conn.access.RUnlock() callback, loaded := t.callbacks[message.Id]
t.callbackAccess.RUnlock()
if !loaded { if !loaded {
continue continue
} }
callback.access.Lock() callback.access.Lock()
select { select {
case <-callback.done: case <-callback.done:
default: default:
callback.message = &message callback.response = &message
close(callback.done) close(callback.done)
} }
callback.access.Unlock() callback.access.Unlock()
} }
} }
type dnsConnection struct {
net.Conn
access sync.RWMutex
done chan struct{}
closeOnce sync.Once
err error
queryId uint16
callbacks map[uint16]*dnsCallback
}
func (c *dnsConnection) Close(err error) {
c.closeOnce.Do(func() {
c.err = err
close(c.done)
})
c.Conn.Close()
}
type dnsCallback struct {
access sync.Mutex
message *mDNS.Msg
done chan struct{}
}

View File

@@ -2,10 +2,230 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.13.0-alpha.20 #### 1.13.0-alpha.35
* Add pre-match support for `auto_redirect` **1**
**1**:
`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).
#### 1.13.0-alpha.34
* Add Chrome Root Store certificate option **1**
* Add new options for ACME DNS-01 challenge providers **2**
* Add Wi-Fi state support for Linux and Windows **3**
* Update naiveproxy to 143.0.7499.109
* Update quic-go to v0.58.0
* Update tailscale to v1.92.4
* Drop support for go1.23 **4**
* Drop support for Android 5.0 **5**
**1**:
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
See [Certificate](/configuration/certificate/#store).
**2**:
See [DNS-01 Challenge](/configuration/shared/dns01_challenge/).
**3**:
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/).
**4**:
Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile.
**5**:
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.
#### 1.12.14
* Fixes and improvements * Fixes and improvements
#### 1.13.0-alpha.33
* Fixes and improvements
#### 1.13.0-alpha.32
* Remove `certificate_public_key_sha256` option for NaiveProxy outbound **1**
* Fixes and improvements
**1**:
Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis.
For this reason, and due to maintenance costs, there is no reason to continue supporting `certificate_public_key_sha256`, which was designed to simplify the use of self-signed certificates.
#### 1.13.0-alpha.31
* Add QUIC support for NaiveProxy outbound **1**
* Add QUIC congestion control option for NaiveProxy **2**
* Fixes and improvements
**1**:
NaiveProxy outbound now supports QUIC.
See [NaiveProxy outbound](/configuration/outbound/naive/#quic).
**2**:
NaiveProxy inbound and outbound now supports configurable QUIC congestion control algorithms, including BBR and BBRv2.
See [NaiveProxy inbound](/configuration/inbound/naive/#quic_congestion_control) and [NaiveProxy outbound](/configuration/outbound/naive/#quic_congestion_control).
#### 1.13.0-alpha.30
* Add ECH support for NaiveProxy outbound **1**
* Add `tls.ech.query_server_name` option **2**
* Fix NaiveProxy outbound on Windows **3**
* Add OpenAI Codex Multiplexer service **4**
* Fixes and improvements
**1**:
See [NaiveProxy outbound](/configuration/outbound/naive/#tls).
**2**:
See [TLS](/configuration/shared/tls/#query_server_name).
**3**:
Each Windows release now includes `libcronet.dll`.
Ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`.
**4**:
See [OCM](/configuration/service/ocm).
#### 1.13.0-alpha.29
* Add UDP over TCP support for naiveproxy outbound **1**
* Fixes and improvements
**1**:
See [NaiveProxy outbound](/configuration/outbound/naive/#udp_over_tcp).
#### 1.13.0-alpha.28
* Add naiveproxy outbound **1**
* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for dial fields **2**
* Update default TCP keep-alive initial period from 10 minutes to 5 minutes
* Update quic-go to v0.57.1
* Fixes and improvements
**1**:
Only available on Apple platforms, Android, Windows and some Linux architectures.
See [NaiveProxy outbound](/configuration/outbound/naive/).
**2**:
See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive).
* __Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
because system extensions require signatures to function, we have had to temporarily halt its release.__
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
#### 1.12.13
* Fix naive inbound
* Fixes and improvements
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
because system extensions require signatures to function, we have had to temporarily halt its release.__
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
#### 1.12.12
* Fixes and improvements
#### 1.13.0-alpha.26
* Update quic-go to v0.55.0
* Fix memory leak in hysteria2
* Fixes and improvements
#### 1.12.11
* Fixes and improvements
#### 1.13.0-alpha.24
* Add Claude Code Multiplexer service **1**
* Fixes and improvements
**1**:
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).
#### 1.13.0-alpha.23
* Fix compatibility with MPTCP **1**
* Fixes and improvements
**1**:
`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues,
but you can change it to bypass the sing-box via the new `exclude_mptcp` option.
See [TUN](/configuration/inbound/tun/#exclude_mptcp).
#### 1.13.0-alpha.22
* Update uTLS to v1.8.1 **1**
* Fixes and improvements
**1**:
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
see https://github.com/refraction-networking/utls/pull/375.
#### 1.12.10
* Update uTLS to v1.8.1 **1**
* Fixes and improvements
**1**:
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
see https://github.com/refraction-networking/utls/pull/375.
#### 1.13.0-alpha.21
* Fix missing mTLS support in client options **1**
* Fixes and improvements
See [TLS](/configuration/shared/tls/).
#### 1.12.9 #### 1.12.9
* Fixes and improvements * Fixes and improvements
@@ -127,7 +347,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go). For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
**7**: **7**:
@@ -189,7 +410,8 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro. The following data was tested
using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
| Version | Stack | MTU | Upload | Download | | Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------| |-------------|--------|-------|--------|----------|
@@ -208,8 +430,8 @@ The following data was tested using [tun_bench](https://github.com/SagerNet/sing
**18**: **18**:
We continue to experience issues updating our sing-box apps on the App Store and Play Store. We continue to experience issues updating our sing-box apps on the App Store and Play Store.
Until we rewrite and resubmit the apps, they are considered irrecoverable. Until we rewrite and resubmit the apps, they are considered irrecoverable.
Therefore, after this release, we will not be repeating this notice unless there is new information. Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15 ### 1.11.15
@@ -490,7 +712,8 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
**2**: **2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action). `resolve` route action now accepts `disable_cache` and other options like in DNS route actions,
see [Route Action](/configuration/route/rule_action).
**3**: **3**:
@@ -521,7 +744,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go). For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3 ### 1.11.3

View File

@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
!!! failure "" !!! failure ""
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected). Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected)
## :material-graph: Requirements ## :material-graph: Requirements
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download ## :material-download: Download
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168) * ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
* TestFlight (Beta) * TestFlight (Beta)
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai) TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot) Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
or sending us your Apple ID [via email](mailto:contact@sagernet.org). or sending us your Apple ID [via email](mailto:contact@sagernet.org).
## :material-file-download: Download (macOS standalone version) ## ~~:material-file-download: Download (macOS standalone version)~~
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm) * ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~
```bash ```bash
brew install sfm # brew install sfm
``` ```
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases) * ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~
## :material-source-repository: Source code ## :material-source-repository: Source code

View File

@@ -4,6 +4,10 @@ icon: material/new-box
!!! question "Since sing-box 1.12.0" !!! question "Since sing-box 1.12.0"
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [Chrome Root Store](#store)
# Certificate # Certificate
### Structure ### Structure
@@ -27,11 +31,12 @@ icon: material/new-box
The default X509 trusted CA certificate list. The default X509 trusted CA certificate list.
| Type | Description | | Type | Description |
|--------------------|---------------------------------------------------------------------------------------------------------------| |--------------------|----------------------------------------------------------------------------------------------------------------|
| `system` (default) | System trusted CA certificates | | `system` (default) | System trusted CA certificates |
| `mozilla` | [Mozilla Included List](https://wiki.mozilla.org/CA/Included_Certificates) with China CA certificates removed | | `mozilla` | [Mozilla Included List](https://wiki.mozilla.org/CA/Included_Certificates) with China CA certificates removed |
| `none` | Empty list | | `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy) with China CA certificates removed |
| `none` | Empty list |
#### certificate #### certificate

View File

@@ -4,6 +4,10 @@ icon: material/new-box
!!! question "自 sing-box 1.12.0 起" !!! question "自 sing-box 1.12.0 起"
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [Chrome Root Store](#store)
# 证书 # 证书
### 结构 ### 结构
@@ -27,11 +31,12 @@ icon: material/new-box
默认的 X509 受信任 CA 证书列表。 默认的 X509 受信任 CA 证书列表。
| 类型 | 描述 | | 类型 | 描述 |
|--------------------|--------------------------------------------------------------------------------------------| |-------------------|--------------------------------------------------------------------------------------------|
| `system`(默认) | 系统受信任的 CA 证书 | | `system`(默认) | 系统受信任的 CA 证书 |
| `mozilla` | [Mozilla 包含列表](https://wiki.mozilla.org/CA/Included_Certificates)(已移除中国 CA 证书) | | `mozilla` | [Mozilla 包含列表](https://wiki.mozilla.org/CA/Included_Certificates)(已移除中国 CA 证书) |
| `none` | 空列表 | | `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy)(已移除中国 CA 证书) |
| `none` | 空列表 |
#### certificate #### certificate

View File

@@ -412,7 +412,7 @@ Match default interface address.
!!! quote "" !!! quote ""
Only supported in graphical clients on Android and Apple platforms. Only supported in graphical clients on Android and Apple platforms, or on Linux.
Match WiFi SSID. Match WiFi SSID.
@@ -420,7 +420,7 @@ Match WiFi SSID.
!!! quote "" !!! quote ""
Only supported in graphical clients on Android and Apple platforms. Only supported in graphical clients on Android and Apple platforms, or on Linux.
Match WiFi BSSID. Match WiFi BSSID.

View File

@@ -411,7 +411,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! quote "" !!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。 仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。
匹配 WiFi SSID。 匹配 WiFi SSID。
@@ -419,7 +419,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! quote "" !!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。 仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。
匹配 WiFi BSSID。 匹配 WiFi BSSID。

View File

@@ -53,7 +53,7 @@ DNS 服务器的地址。
| `HTTP3` | `h3://8.8.8.8/dns-query` | | `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` | | `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` | | `DHCP` | `dhcp://auto``dhcp://en0` |
| [FakeIP](/configuration/dns/fakeip/) | `fakeip` | | [FakeIP](/zh/configuration/dns/fakeip/) | `fakeip` |
!!! warning "" !!! warning ""

View File

@@ -4,7 +4,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.13.0" !!! quote "Changes in sing-box 1.13.0"
:material-plus: [prefer_go](#prefer_go) :material-plus: [prefer_go](#prefer_go)
!!! question "Since sing-box 1.12.0" !!! question "Since sing-box 1.12.0"

View File

@@ -3,7 +3,7 @@
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [store_rdrc](#store_rdrc) :material-plus: [store_rdrc](#store_rdrc)
:material-plus: [rdrc_timeout](#rdrc_timeout) :material-plus: [rdrc_timeout](#rdrc_timeout)
### Structure ### Structure

View File

@@ -3,7 +3,7 @@
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [store_rdrc](#store_rdrc) :material-plus: [store_rdrc](#store_rdrc)
:material-plus: [rdrc_timeout](#rdrc_timeout) :material-plus: [rdrc_timeout](#rdrc_timeout)
### 结构 ### 结构

View File

@@ -1,20 +1,25 @@
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [quic_congestion_control](#quic_congestion_control)
### Structure ### Structure
```json ```json
{ {
"type": "naive", "type": "naive",
"tag": "naive-in", "tag": "naive-in",
"network": "udp", "network": "udp",
...
// Listen Fields
... // Listen Fields "users": [
{
"users": [ "username": "sekai",
{ "password": "password"
"username": "sekai", }
"password": "password" ],
} "quic_congestion_control": "",
], "tls": {}
"tls": {}
} }
``` ```
@@ -36,6 +41,23 @@ Both if empty.
Naive users. Naive users.
#### quic_congestion_control
!!! question "Since sing-box 1.13.0"
QUIC congestion control algorithm.
| Algorithm | Description |
|----------------|---------------------------------|
| `bbr` | BBR |
| `bbr_standard` | BBR (Standard version) |
| `bbr2` | BBRv2 |
| `bbr2_variant` | BBRv2 (An experimental variant) |
| `cubic` | CUBIC |
| `reno` | New Reno |
`bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on).
#### tls #### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound). TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@@ -1,20 +1,25 @@
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [quic_congestion_control](#quic_congestion_control)
### 结构 ### 结构
```json ```json
{ {
"type": "naive", "type": "naive",
"tag": "naive-in", "tag": "naive-in",
"network": "udp", "network": "udp",
... // 监听字段 ... // 监听字段
"users": [ "users": [
{ {
"username": "sekai", "username": "sekai",
"password": "password" "password": "password"
} }
], ],
"tls": {} "quic_congestion_control": "",
"tls": {}
} }
``` ```
@@ -36,6 +41,23 @@
Naive 用户。 Naive 用户。
#### quic_congestion_control
!!! question "Since sing-box 1.13.0"
QUIC 拥塞控制算法。
| 算法 | 描述 |
|----------------|--------------------|
| `bbr` | BBR |
| `bbr_standard` | BBR (标准版) |
| `bbr2` | BBRv2 |
| `bbr2_variant` | BBRv2 (一种试验变体) |
| `cubic` | CUBIC |
| `reno` | New Reno |
默认使用 `bbr`NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -48,9 +48,9 @@
} }
``` ```
### Listen Fields ### 监听字段
See [Listen Fields](/configuration/shared/listen/) for details. 参阅 [监听字段](/zh/configuration/shared/listen/)
### 字段 ### 字段

View File

@@ -2,6 +2,12 @@
icon: material/new-box icon: material/new-box
--- ---
!!! 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)
!!! quote "Changes in sing-box 1.12.0" !!! quote "Changes in sing-box 1.12.0"
:material-plus: [loopback_address](#loopback_address) :material-plus: [loopback_address](#loopback_address)
@@ -34,7 +40,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)
:material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain)
!!! quote "Changes in sing-box 1.8.0" !!! quote "Changes in sing-box 1.8.0"
@@ -63,6 +69,9 @@ icon: material/new-box
"auto_redirect": true, "auto_redirect": true,
"auto_redirect_input_mark": "0x2023", "auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024", "auto_redirect_output_mark": "0x2024",
"auto_redirect_reset_mark": "0x2025",
"auto_redirect_nfqueue": 100,
"exclude_mptcp": false,
"loopback_address": [ "loopback_address": [
"10.7.0.1" "10.7.0.1"
], ],
@@ -278,6 +287,36 @@ Connection output mark used by `auto_redirect`.
`0x2024` is used by default. `0x2024` is used by default.
#### auto_redirect_reset_mark
!!! question "Since sing-box 1.13.0"
Connection reset mark used by `auto_redirect` pre-matching.
`0x2025` is used by default.
#### auto_redirect_nfqueue
!!! question "Since sing-box 1.13.0"
NFQueue number used by `auto_redirect` pre-matching.
`100` is used by default.
#### exclude_mptcp
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
MPTCP cannot be transparently proxied due to protocol limitations.
Such traffic is usually created by Apple systems.
When enabled, MPTCP connections will bypass sing-box and connect directly, otherwise, will be rejected to avoid errors by default.
#### loopback_address #### loopback_address
!!! question "Since sing-box 1.12.0" !!! question "Since sing-box 1.12.0"

View File

@@ -2,6 +2,12 @@
icon: material/new-box icon: material/new-box
--- ---
!!! 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)
!!! quote "sing-box 1.12.0 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-plus: [loopback_address](#loopback_address) :material-plus: [loopback_address](#loopback_address)
@@ -22,7 +28,7 @@ icon: material/new-box
:material-delete-clock: [inet6_route_address](#inet6_route_address) :material-delete-clock: [inet6_route_address](#inet6_route_address)
:material-plus: [route_exclude_address](#route_address) :material-plus: [route_exclude_address](#route_address)
:material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)
:material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)
:material-plus: [iproute2_table_index](#iproute2_table_index) :material-plus: [iproute2_table_index](#iproute2_table_index)
:material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [iproute2_rule_index](#iproute2_table_index)
:material-plus: [auto_redirect](#auto_redirect) :material-plus: [auto_redirect](#auto_redirect)
@@ -34,7 +40,7 @@ icon: material/new-box
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)
:material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain)
!!! quote "sing-box 1.8.0 中的更改" !!! quote "sing-box 1.8.0 中的更改"
@@ -63,6 +69,9 @@ icon: material/new-box
"auto_redirect": true, "auto_redirect": true,
"auto_redirect_input_mark": "0x2023", "auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024", "auto_redirect_output_mark": "0x2024",
"auto_redirect_reset_mark": "0x2025",
"auto_redirect_nfqueue": 100,
"exclude_mptcp": false,
"loopback_address": [ "loopback_address": [
"10.7.0.1" "10.7.0.1"
], ],
@@ -277,6 +286,36 @@ tun 接口的 IPv6 前缀。
默认使用 `0x2024` 默认使用 `0x2024`
#### auto_redirect_reset_mark
!!! question "自 sing-box 1.13.0 起"
`auto_redirect` 预匹配使用的连接重置标记。
默认使用 `0x2025`
#### auto_redirect_nfqueue
!!! question "自 sing-box 1.13.0 起"
`auto_redirect` 预匹配使用的 NFQueue 编号。
默认使用 `100`
#### exclude_mptcp
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux且需要 nftables`auto_route``auto_redirect` 已启用。
由于协议限制MPTCP 无法被透明代理。
此类流量通常由 Apple 系统创建。
启用时MPTCP 连接将绕过 sing-box 直接连接,否则,将被拒绝以避免错误。
#### loopback_address #### loopback_address
!!! question "自 sing-box 1.12.0 起" !!! question "自 sing-box 1.12.0 起"

View File

@@ -36,6 +36,7 @@
| `dns` | [DNS](./dns/) | | `dns` | [DNS](./dns/) |
| `selector` | [Selector](./selector/) | | `selector` | [Selector](./selector/) |
| `urltest` | [URLTest](./urltest/) | | `urltest` | [URLTest](./urltest/) |
| `naive` | [NaiveProxy](./naive/) |
#### tag #### tag

View File

@@ -36,6 +36,7 @@
| `dns` | [DNS](./dns/) | | `dns` | [DNS](./dns/) |
| `selector` | [Selector](./selector/) | | `selector` | [Selector](./selector/) |
| `urltest` | [URLTest](./urltest/) | | `urltest` | [URLTest](./urltest/) |
| `naive` | [NaiveProxy](./naive/) |
#### tag #### tag

View File

@@ -0,0 +1,114 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.13.0"
### Structure
```json
{
"type": "naive",
"tag": "naive-out",
"server": "127.0.0.1",
"server_port": 443,
"username": "sekai",
"password": "password",
"insecure_concurrency": 0,
"extra_headers": {},
"udp_over_tcp": false | {},
"quic": false,
"quic_congestion_control": "",
"tls": {},
... // Dial Fields
}
```
!!! warning "Platform Support"
NaiveProxy outbound is only available on Apple platforms, Android, Windows and certain Linux builds.
**Official Release Build Variants:**
| Build Variant | Platforms | Description |
|---------------|-----------|-------------|
| (default) | Linux amd64/arm64 | purego build with `libcronet.so` included |
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO build dynamically linked with glibc, requires glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64 | CGO build statically linked with musl, no system requirements |
| (default) | Windows amd64/arm64 | purego build with `libcronet.dll` included |
**Runtime Requirements:**
- **Linux purego**: `libcronet.so` must be in the same directory as the sing-box binary or in system library path
- **Windows**: `libcronet.dll` must be in the same directory as `sing-box.exe` or in a directory listed in `PATH`
For self-built binaries, see [Build from source](/installation/build-from-source/#with_naive_outbound).
### Fields
#### server
==Required==
The server address.
#### server_port
==Required==
The server port.
#### username
Authentication username.
#### password
Authentication password.
#### insecure_concurrency
Number of concurrent tunnel connections. Multiple connections make the tunneling easier to detect through traffic analysis, which defeats the purpose of NaiveProxy's design to resist traffic analysis.
#### extra_headers
Extra headers to send in HTTP requests.
#### udp_over_tcp
UDP over TCP protocol settings.
See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details.
#### quic
Use QUIC instead of HTTP/2.
#### quic_congestion_control
QUIC congestion control algorithm.
| Algorithm | Description |
|-----------|-------------|
| `bbr` | BBR |
| `bbr2` | BBRv2 |
| `cubic` | CUBIC |
| `reno` | New Reno |
`bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on).
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
Only `server_name`, `certificate`, `certificate_path` and `ech` are supported.
Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis, and should not be used in production.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,114 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.13.0 起"
### 结构
```json
{
"type": "naive",
"tag": "naive-out",
"server": "127.0.0.1",
"server_port": 443,
"username": "sekai",
"password": "password",
"insecure_concurrency": 0,
"extra_headers": {},
"udp_over_tcp": false | {},
"quic": false,
"quic_congestion_control": "",
"tls": {},
... // 拨号字段
}
```
!!! warning "平台支持"
NaiveProxy 出站仅在 Apple 平台、Android、Windows 和特定 Linux 构建上可用。
**官方发布版本区别:**
| 构建变体 | 平台 | 说明 |
|-----------|------------------------|------------------------------------------|
| (默认) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO 构建,动态链接 glibc要求 glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64 | CGO 构建,静态链接 musl无系统要求 |
| (默认) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
**运行时要求:**
- **Linux purego**`libcronet.so` 必须位于 sing-box 二进制文件相同目录或系统库路径中
- **Windows**`libcronet.dll` 必须位于 `sing-box.exe` 相同目录或 `PATH` 中的任意目录
自行构建请参阅 [从源代码构建](/zh/installation/build-from-source/#with_naive_outbound)。
### 字段
#### server
==必填==
服务器地址。
#### server_port
==必填==
服务器端口。
#### username
认证用户名。
#### password
认证密码。
#### insecure_concurrency
并发隧道连接数。多连接使隧道更容易被流量分析检测,违背 NaiveProxy 抵抗流量分析的设计目的。
#### extra_headers
HTTP 请求中发送的额外头部。
#### udp_over_tcp
UDP over TCP 配置。
参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。
#### quic
使用 QUIC 代替 HTTP/2。
#### quic_congestion_control
QUIC 拥塞控制算法。
| 算法 | 描述 |
|------|------|
| `bbr` | BBR |
| `bbr2` | BBRv2 |
| `cubic` | CUBIC |
| `reno` | New Reno |
默认使用 `bbr`NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。
#### tls
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
只有 `server_name``certificate``certificate_path``ech` 是被支持的。
自签名证书会显著改变流量行为,违背了 NaiveProxy 旨在抵抗流量分析的设计初衷,不应该在生产环境中使用。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@@ -66,7 +66,7 @@ UDP 包中继模式
#### udp_over_stream #### udp_over_stream
这是 TUIC 的 [UDP over TCP 协议](/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。 这是 TUIC 的 [UDP over TCP 协议](/zh/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。
此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。 此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。

View File

@@ -12,7 +12,7 @@ icon: material/delete-clock
!!! quote "Changes in sing-box 1.8.0" !!! quote "Changes in sing-box 1.8.0"
:material-plus: [gso](#gso) :material-plus: [gso](#gso)
### Structure ### Structure

View File

@@ -12,7 +12,7 @@ icon: material/delete-clock
!!! quote "sing-box 1.8.0 中的更改" !!! quote "sing-box 1.8.0 中的更改"
:material-plus: [gso](#gso) :material-plus: [gso](#gso)
### 结构 ### 结构

View File

@@ -62,7 +62,7 @@ icon: material/alert-decagram
!!! question "自 sing-box 1.8.0 起" !!! question "自 sing-box 1.8.0 起"
一组 [规则集](/configuration/rule-set/)。 一组 [规则集](/zh/configuration/rule-set/)。
#### final #### final

View File

@@ -428,20 +428,16 @@ Match default interface address.
#### wifi_ssid #### wifi_ssid
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match WiFi SSID. Match WiFi SSID.
See [Wi-Fi State](/configuration/shared/wifi-state/) for details.
#### wifi_bssid #### wifi_bssid
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Match WiFi BSSID. Match WiFi BSSID.
See [Wi-Fi State](/configuration/shared/wifi-state/) for details.
#### preferred_by #### preferred_by
!!! question "Since sing-box 1.13.0" !!! question "Since sing-box 1.13.0"

View File

@@ -425,20 +425,16 @@ icon: material/new-box
#### wifi_ssid #### wifi_ssid
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi SSID。 匹配 WiFi SSID。
参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。
#### wifi_bssid #### wifi_bssid
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配 WiFi BSSID。 匹配 WiFi BSSID。
参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。
#### preferred_by #### preferred_by
!!! question "自 sing-box 1.13.0 起" !!! question "自 sing-box 1.13.0 起"

View File

@@ -4,6 +4,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.13.0" !!! quote "Changes in sing-box 1.13.0"
:material-plus: [bypass](#bypass)
:material-alert: [reject](#reject) :material-alert: [reject](#reject)
!!! quote "Changes in sing-box 1.12.0" !!! quote "Changes in sing-box 1.12.0"
@@ -44,6 +45,40 @@ Tag of target outbound.
See `route-options` fields below. See `route-options` fields below.
### bypass
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux with `auto_redirect` enabled.
```json
{
"action": "bypass",
"outbound": "",
... // route-options Fields
}
```
`bypass` bypasses sing-box at the kernel level for auto redirect connections in pre-match.
For non-auto-redirect connections and already established connections,
if `outbound` is specified, the behavior is the same as `route`;
otherwise, the rule will be skipped.
#### outbound
Tag of target outbound.
If not specified, the rule only matches in [pre-match](/configuration/shared/pre-match/)
from auto redirect, and will be skipped in other contexts.
#### route-options Fields
See `route-options` fields below.
### reject ### reject
!!! quote "Changes in sing-box 1.13.0" !!! quote "Changes in sing-box 1.13.0"

View File

@@ -4,6 +4,7 @@ icon: material/new-box
!!! quote "sing-box 1.13.0 中的更改" !!! quote "sing-box 1.13.0 中的更改"
:material-plus: [bypass](#bypass)
:material-alert: [reject](#reject) :material-alert: [reject](#reject)
!!! quote "sing-box 1.12.0 中的更改" !!! quote "sing-box 1.12.0 中的更改"
@@ -40,6 +41,37 @@ icon: material/new-box
参阅下方的 `route-options` 字段。 参阅下方的 `route-options` 字段。
### bypass
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux且需要启用 `auto_redirect`
```json
{
"action": "bypass",
"outbound": "",
... // route-options 字段
}
```
`bypass` 在预匹配中为 auto redirect 连接在内核层面绕过 sing-box。
对于非 auto redirect 连接和已建立的连接,如果指定了 `outbound`,行为与 `route` 相同;否则规则将被跳过。
#### outbound
目标出站的标签。
如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
#### route-options 字段
参阅下方的 `route-options` 字段。
### reject ### reject
!!! quote "sing-box 1.13.0 中的更改" !!! quote "sing-box 1.13.0 中的更改"

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