Compare commits

..

270 Commits

Author SHA1 Message Date
renovate[bot]
c9f911316e [dependencies] Update github-actions 2026-03-26 17:10:31 +00:00
世界
5b29fd3be4 ccm,ocm: add request ID context to HTTP request logging 2026-03-14 16:14:24 +08:00
世界
016e5e1b12 service/ocm: add default OpenAI-Beta header and log websocket error body
The upstream OpenAI WebSocket endpoint requires the
OpenAI-Beta: responses_websockets=2026-02-06 header. Set it
automatically when the client doesn't provide it.

Also capture and log the response body on non-429 WebSocket
handshake failures to surface the actual error from upstream.
2026-03-14 16:07:58 +08:00
世界
92b3bde862 ccm,ocm: strip reverse proxy headers from upstream responses 2026-03-14 15:58:33 +08:00
世界
64f7349fca service/ocm: unify websocket logging with HTTP request logging 2026-03-14 15:52:27 +08:00
世界
527372ba74 ccm,ocm: auto-detect plan weight for external credentials via status endpoint 2026-03-14 15:44:04 +08:00
世界
c6c07cb52f service/ccm: update oauth token URL and remove unnecessary Accept header 2026-03-14 15:19:59 +08:00
世界
913f033d1a ccm,ocm: improve balancer least_used with plan-weighted scoring and reset urgency
Scale remaining capacity by plan weight (Pro=1, Max 5x=5, Max 20x=10
for CCM; Plus=1, Pro=10 for OCM) so higher-tier accounts contribute
proportionally more. Factor in weekly reset proximity so credentials
about to reset are preferred ("use it or lose it").

Auto-detect plan weight from subscriptionType + rateLimitTier (CCM)
or plan_type (OCM). Fetch /api/oauth/profile when rateLimitTier is
missing from the credential file. External credentials accept a
manual plan_weight option.
2026-03-14 15:10:13 +08:00
世界
688f8cc4ef service/ocm: only log new credential assignments and add websocket logging 2026-03-14 14:44:24 +08:00
世界
de51879ae9 service/ccm: only log new credential assignments and show context window in model 2026-03-14 14:31:21 +08:00
世界
2943e8e5f0 ccm,ocm: mark credentials unusable on usage poll failure and trigger poll on upstream error 2026-03-14 14:31:21 +08:00
世界
e2b2af8322 service/ccm: allow extended context (1m) for all credentials
1m context is now available to all subscribers and no longer
consumes Extra Usage.
2026-03-14 13:48:23 +08:00
世界
c8d8d0a3e7 service/ccm: reject fast-mode external credentials 2026-03-13 23:31:19 +08:00
世界
8cd7713ca9 ccm,ocm: fix reserveWeekly default and remove dead reserve fields 2026-03-13 23:29:06 +08:00
世界
566abb00cd ccm,ocm: add limit options and fix aggregated utilization scaling
Add limit_5h and limit_weekly options as alternatives to reserve_5h
and reserve_weekly for capping credential utilization. The two are
mutually exclusive per window.

Fix computeAggregatedUtilization to scale per-credential utilization
relative to each credential's cap before averaging, so external users
see correct available capacity regardless of per-credential caps.

Fix pickLeastUsed to compare remaining capacity (cap - utilization)
instead of raw utilization, ensuring fair comparison across credentials
with different caps.
2026-03-13 23:13:44 +08:00
世界
ae7550b465 ocm: preserve websocket rate limit event fields 2026-03-13 22:24:08 +08:00
世界
63d4cdffef ocm: rewrite codex.rate_limits WebSocket events for external users
The HTTP path rewrites utilization headers for external users via
rewriteResponseHeadersForExternalUser to show aggregated values.
The WebSocket upgrade headers were also rewritten, but in-band
codex.rate_limits events were forwarded unmodified, leaking
per-credential utilization to external users.
2026-03-13 21:54:47 +08:00
世界
5516d7b045 ccm,ocm: fix passive usage update for WebSocket connections
WebSocket 101 upgrade responses do not include utilization headers
(confirmed via codex CLI source). Rate limit data is delivered
exclusively through in-band events (codex.rate_limits and error
events with status 429).

Previously, updateStateFromHeaders unconditionally bumped lastUpdated
even when no utilization headers were found, which suppressed polling
and left credential utilization permanently stale during WebSocket
sessions.

- Only bump lastUpdated when actual utilization data is parsed
- Parse in-band codex.rate_limits events to update credential state
- Detect in-band 429 error events to markRateLimited
- Fix WebSocket 429 retry to update old credential state before retry
2026-03-13 21:42:05 +08:00
世界
c639c27cdb ccm,ocm: fix reverse session shutdown race 2026-03-13 21:31:23 +08:00
世界
f0022f59a2 ccm,ocm: block utilization decrease within same rate-limit window
updateStateFromHeaders unconditionally applied header utilization
values even when they were lower than the current state, causing
poll-sourced values to be overwritten by stale header values.

Parse reset timestamps before utilization and only allow decreases
when the reset timestamp changes (indicating a new rate-limit
window). Also add math.Ceil to CCM external credential for
consistency with default credential.
2026-03-13 21:06:32 +08:00
世界
9e7d863ee7 ccm,ocm: fix connector-side bufio data loss in reverse proxy
connectorConnect() creates a bufio.NewReader to read the HTTP 101
upgrade response, but then passes the raw conn to yamux.Server().
If TCP coalesces the 101 response with initial yamux frames, the
bufio reader over-reads into its buffer and those bytes are lost
to yamux, causing session failure.

Wrap the bufio.Reader and raw conn into a bufferedConn so yamux
reads through the buffer first.
2026-03-13 21:06:32 +08:00
世界
d5c6c6aed2 ccm,ocm: fix data race on reverseContext/reverseCancel
InterfaceUpdated() writes reverseContext and reverseCancel without
synchronization while connectorLoop/connectorConnect goroutines
read them concurrently. close() also accesses reverseCancel without
a lock.

Fix by extending reverseAccess mutex to protect these fields:
- Add getReverseContext()/resetReverseContext() methods
- Pass context as parameter to connectorConnect
- Merge close() into a single lock acquisition
- Use resetReverseContext() in InterfaceUpdated()
2026-03-13 21:06:32 +08:00
世界
4d89d732e2 ccm,ocm: reset connector backoff after successful connection
The consecutiveFailures counter in connectorLoop never resets,
causing backoff to permanently cap at 30-45s even after a
connection that served successfully for hours.

Reset the counter when connectorConnect ran for at least one
minute, indicating a successful session rather than a transient
dial/handshake failure.
2026-03-13 21:05:50 +08:00
世界
f6821be8a3 ccm,ocm: unify HTTP request retry with fast retry and exponential backoff 2026-03-13 20:05:54 +08:00
世界
03b01efe49 Fix reverse external credential handling 2026-03-13 19:36:51 +08:00
世界
16aeba8ec0 ccm,ocm: add reverse proxy support for external credentials
Allow two CCM/OCM instances to share credentials when only one has a
public IP, using yamux-multiplexed reverse connections.

Three credential modes:
- Normal: URL set, reverse=false — standard HTTP proxy
- Receiver: URL empty — waits for incoming reverse connection
- Connector: URL set, reverse=true — dials out to establish connection

Extend InterfaceUpdated to services so network changes trigger
reverse connection reconnection.
2026-03-13 18:51:02 +08:00
世界
283a5aacee ccm,ocm: strip reverse proxy headers before forwarding to upstream 2026-03-13 04:54:11 +08:00
世界
8d852bba9b ccm,ocm,ssmapi: fix HTTP/2 over TLS with h2c handler
aTLS.NewListener returns *LazyConn, not *tls.Conn, so Go's
http.Server cannot detect TLS via type assertion and falls back
to HTTP/1.x. When ALPN negotiates h2, the client sends HTTP/2
frames that the server fails to parse, causing HTTP 520 errors
behind Cloudflare.

Wrap HTTP handlers with h2c.NewHandler to intercept the HTTP/2
client preface and dispatch to http2.Server.ServeConn, consistent
with DERP, v2rayhttp, naive, and v2raygrpclite services.
2026-03-13 04:52:31 +08:00
世界
29c8794f45 ccm,ocm: check credential file writability before token refresh
Refuse to refresh tokens when the credential file is not writable,
preventing server-side invalidation of the old refresh token that
would make the credential permanently unusable after restart.
2026-03-13 03:27:42 +08:00
世界
c8d593503f ccm,ocm: watch credential_path and allow delayed credentials 2026-03-13 02:47:06 +08:00
世界
a8934be7cd ccm/ocm: Add external credential support for cross-instance usage sharing
Extract credential interface from *defaultCredential to support both
default (OAuth) and external (remote proxy) credential types. External
credentials proxy requests to a remote ccm/ocm instance with bearer
token auth, poll a /status endpoint for utilization, and parse
aggregated rate limit headers from responses.

Add allow_external_usage user flag to control whether balancer/fallback
providers may select external credentials. Add status endpoint
(/ccm/v1/status, /ocm/v1/status) returning averaged utilization across
eligible credentials. Rewrite response rate limit headers for external
users with aggregated values.
2026-03-13 01:47:45 +08:00
世界
7aef716ebc ccm/ocm: Add multi-credential support with balancer and fallback strategies 2026-03-13 00:50:04 +08:00
世界
7df171ff20 Bump version 2026-03-11 21:31:42 +08:00
世界
46eda3e96f cronet-go: Update chromium to 145.0.7632.159 2026-03-11 21:31:42 +08:00
世界
727a9d18d6 documentation: Update descriptions for neighbor rules 2026-03-11 21:31:42 +08:00
世界
20f60b8c7b Add macOS support for MAC and hostname rule items 2026-03-11 21:31:42 +08:00
世界
84b0ddff7f Add Android support for MAC and hostname rule items 2026-03-11 21:31:42 +08:00
世界
811ea13b73 Add MAC and hostname rule items 2026-03-11 21:31:41 +08:00
世界
bdb90f0a01 Bump version 2026-03-11 21:30:11 +08:00
世界
c9ab6458fa tun:Fix auto_redirect dropping SO_BINDTODEVICE traffic 2026-03-11 21:30:11 +08:00
世界
16a249f672 tailscale: Fix system interface rules 2026-03-11 21:30:03 +08:00
世界
54468a1a2a platform: Add f-droid update helpers 2026-03-11 20:41:29 +08:00
世界
8289bbd846 Add Alpine APK packaging to CI build
Add fpm-based Alpine APK packaging alongside existing DEB/RPM/Pacman
packages. Alpine APKs use `linux` in the filename to distinguish from
OpenWrt APKs which use the `openwrt` prefix.
2026-03-11 20:41:29 +08:00
世界
49c450d942 ccm/ocm: Fix missing metering for 1M context and /fast mode
CCM: Fix 1M context detection - use prefix match for versioned
beta strings (e.g. "context-1m-2025-08-07") and include cache
tokens in the 200K threshold check per Anthropic billing docs.

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

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

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

Wire compression (CCM→Anthropic) is preserved — Transport still negotiates gzip.
Only CCM→localhost path is affected; compression on loopback has no practical
benefit.
2026-03-09 20:06:34 +08:00
世界
e343cec4d5 Fix legacy DNS defaults on final transport 2026-03-09 20:06:34 +08:00
世界
d58efc5d01 cronet-go: Fix library search path 2026-03-09 20:06:34 +08:00
世界
4b26ab16fb Bump version 2026-03-07 16:13:23 +08:00
世界
0e27312eda Update Go to 1.25.8 2026-03-07 16:13:23 +08:00
世界
4e0a953b98 sing: Revert "Relax domain name validation to support non-standard characters" 2026-03-07 15:44:40 +08:00
世界
27c5b0b1af Fix DNS exchange failure and recursion deadlock in connector
Co-authored-by: everyx <lunt.luo@gmail.com>
2026-03-06 15:31:22 +08:00
dyhkwong
84019b06d9 Fix v2ray HTTP transport server 2026-03-06 10:13:39 +08:00
世界
7fd21f8bf4 Bump version 2026-03-05 21:46:27 +08:00
世界
88695b0d1f Rename branches and update release workflows
stable-next → oldstable, main-next → stable, dev-next → testing, new unstable
2026-03-05 21:12:02 +08:00
世界
fb269c9032 tun: Fix darwin batch loop not exit on EBADF 2026-03-05 20:38:19 +08:00
世界
e62dc7bfa2 Fix rule_set_ip_cidr_accept_empty not working 2026-03-04 11:48:22 +08:00
世界
f295e195b5 tailscale: Fix netstack TCP connections with system interface 2026-03-03 22:06:54 +08:00
世界
ab76062a41 Fix fake-ip address allocation 2026-03-03 21:37:24 +08:00
世界
d14417d392 Fix naive client close 2026-03-03 21:21:09 +08:00
世界
96c5c27610 sing: reject IP literals in IsDomainName 2026-03-03 21:21:09 +08:00
世界
91f92bee49 release: Unify default build tags and linker flags into shared files
Move hardcoded build tags and ldflags from Makefile, Dockerfile, CI
workflows, and local build scripts into canonical files under release/:

- release/DEFAULT_BUILD_TAGS (Linux common archs, Darwin, Android)
- release/DEFAULT_BUILD_TAGS_WINDOWS (includes with_purego)
- release/DEFAULT_BUILD_TAGS_OTHERS (no with_naive_outbound)
- release/LDFLAGS (shared linker flags)
2026-03-03 21:21:09 +08:00
世界
1803471e02 endpoint: Fix UDP resolved destination 2026-03-02 13:55:26 +08:00
世界
3de56d344e Update external dependencies 2026-03-02 06:53:10 +08:00
世界
c71abbdfb8 Update dependencies 2026-03-02 06:52:35 +08:00
世界
ed15121e95 sing: Relax domain name validation to support non-standard characters 2026-03-01 19:45:19 +08:00
世界
46c6945da5 documentation: Update mkdcos-material 2026-03-01 18:37:31 +08:00
traitman
1beb4cb002 clash-api: Fix websocket connection not closed after config reload via SIGHUP
Co-authored-by: TraitMan <traitman@maildog.top>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-01 12:30:43 +08:00
dyhkwong
4c65fea1ac Fix IPv6 local DNS on Windows 2026-03-01 12:30:43 +08:00
世界
8ae93a98e5 Remove overdue deprecated features 2026-03-01 12:30:43 +08:00
世界
6da7e538e1 Bump version 2026-02-28 14:42:39 +08:00
世界
13e6ba4cb2 Update tfo-go 2026-02-27 19:55:32 +08:00
世界
93b7328c3f Fix missing Tailscale in ProxyDisplayName 2026-02-27 19:39:52 +08:00
世界
11dc5bcbe1 Fixes in cronet-go 2026-02-27 19:39:52 +08:00
世界
fa3ab87b11 platform: Fix gorelease build 2026-02-27 15:07:16 +08:00
世界
9bd9e9a58b dialer: use KeepAliveConfig for TCP keepalive 2026-02-27 14:58:06 +08:00
世界
9d6dee7451 release: Fix pacman package 2026-02-27 14:58:06 +08:00
世界
9c2cdc7203 Fix per-outbound bind_interface 2026-02-27 14:58:06 +08:00
世界
65150f5cc3 platform: Improve OOM killer for iOS 2026-02-27 14:58:06 +08:00
世界
21a1512e6c tailscale: Fix AdvertiseTags 2026-02-27 14:58:06 +08:00
世界
cf4791f1ad platform: Improve iOS OOM killer 2026-02-26 14:13:32 +08:00
世界
0bc66e5a56 service/ccm,ocm: Fixes and improvements 2026-02-26 13:36:46 +08:00
世界
d48236da94 Fix wireguard reserved 2026-02-24 15:49:52 +08:00
世界
4c05d7b888 Add advertise tags support for Tailscale endpoint 2026-02-24 15:31:57 +08:00
世界
94ed42caf1 Bump version 2026-02-23 18:17:47 +08:00
世界
e0c18cc3d4 tun: Fix nftablesCreateLocalAddressSets 2026-02-23 18:17:47 +08:00
世界
0817c25f4c release: Fix Docker build for loong64 and mipsle 2026-02-23 16:31:19 +08:00
世界
7745a97cca daemon: Fix started service leak 2026-02-23 14:49:58 +08:00
世界
9bcd715d31 Bump version 2026-02-21 13:55:31 +08:00
世界
6a95c66bc7 Pin Go version to 1.25.7 2026-02-21 13:55:31 +08:00
世界
b5800847ae More linux builds for naive 2026-02-21 13:55:31 +08:00
世界
aa85cbb86e Treat H3 RequestCanceled as closed 2026-02-21 09:31:11 +08:00
世界
c59991420e Minor fixes for naive 2026-02-18 01:26:29 +08:00
世界
c0304b8362 Bump version 2026-02-16 12:46:43 +08:00
世界
d1f1271a02 quic-go: Minor fixes 2026-02-16 12:46:29 +08:00
世界
de4fdbe553 platform: Add semver helper 2026-02-16 11:28:54 +08:00
世界
804606042f Bump version 2026-02-15 21:13:55 +08:00
世界
53f2db3f97 platform: Add windows build 2026-02-15 21:10:44 +08:00
世界
1f2fdec89d release: Fix update_apple_version command 2026-02-15 21:09:14 +08:00
世界
8714c157c9 Fix matching multi predefined 2026-02-15 21:09:06 +08:00
世界
657fba4ca5 Fix matching rule-set invert 2026-02-15 21:08:33 +08:00
世界
0a69621207 wireguard: Fix missing fallback for gso 2026-02-15 21:08:26 +08:00
世界
58ccf82e0b Bump version 2026-02-09 15:50:51 +08:00
世界
ceab244329 tuic: Fix udp context 2026-02-09 15:50:51 +08:00
世界
58fcdceca2 Fix naive padding 2026-02-09 15:50:51 +08:00
世界
98af3c0ad6 experimental: New FFI 2026-02-09 15:50:51 +08:00
世界
172a9d5e4e Standardize gomobile usages 2026-02-07 15:52:26 +08:00
世界
aba8346bd6 Fix DNS cache lock goroutine leak
The cache deduplication in Client.Exchange uses a channel-based lock
per DNS question. Waiting goroutines blocked on <-cond without context
awareness, causing them to accumulate indefinitely when the owning
goroutine's transport call stalls. Add select on ctx.Done() so waiters
respect context cancellation and timeouts.
2026-02-06 22:28:35 +08:00
世界
d8e269e0ac socks: Fix "Fix missing UDP timeout" 2026-02-06 22:26:45 +08:00
世界
c45ea8dfac Recover from bbolt panics on corrupted database
When bbolt encounters corrupted page data at runtime, it panics
instead of returning an error. Wrap all DB transactions with
recover to catch these panics, delete the corrupted database
file, and reopen a fresh one.
2026-02-06 19:35:32 +08:00
世界
a2d313c59b Bump version 2026-02-05 20:28:25 +08:00
世界
15722b06dd Update Go to 1.25.7 2026-02-05 17:49:06 +08:00
世界
d230dae0a5 Fix vmess crash 2026-02-05 17:23:49 +08:00
世界
e11dbf3a8e bufio: Refactor copy 2026-02-05 12:03:03 +08:00
世界
baa9f29f0d documentation: Update release changelog 2026-02-05 12:03:03 +08:00
世界
55b6e7dbfe socks: Fix missing UDP timeout 2026-02-05 12:03:03 +08:00
世界
a05e05a47c Fix random iproute2 table index was incorrectly removed 2026-02-02 14:15:55 +08:00
世界
c1dc6cb0fb Bump version 2026-02-01 12:29:57 +08:00
世界
432fe1b3c9 Disable rp filter atomically 2026-02-01 10:49:12 +08:00
世界
8dd8897fd8 Fix varbin serialization 2026-02-01 10:48:05 +08:00
世界
ff58edb1c1 Bump version 2026-01-30 14:05:29 +08:00
世界
79bab39502 Fix auto_redirect fallback rule 2026-01-30 11:42:56 +08:00
世界
a4d5d59901 Minor fixes 2026-01-29 13:40:34 +08:00
世界
1af14a0237 Remove varbin usages 2026-01-29 13:40:34 +08:00
世界
944a9986d9 release: Always build tailscale for iOS and tvOS 2026-01-29 13:40:34 +08:00
Balthild
60a1e4c866 Add acmedns support 2026-01-17 20:52:43 +08:00
世界
5d67c131fa documentation: Bump version 2026-01-17 19:21:19 +08:00
世界
b9cc87d35a Skip strict routing in Windows versions below Windows 10 2026-01-17 19:21:19 +08:00
世界
490d501257 Fix trafficontrol Manager 2026-01-17 19:16:56 +08:00
世界
725e4adc46 release: Update android command 2026-01-17 19:16:56 +08:00
世界
4a14d39cad release: Log build ID during TestFlight publishing 2026-01-17 19:16:56 +08:00
世界
8ec58c96f5 Fix naive outbound on iOS 2026-01-17 19:15:56 +08:00
世界
e8450b2e61 platform: Refactor CommandClient & Connections 2026-01-17 05:50:39 +08:00
世界
30c3855e4b Fix logic issues with BBR impl 2026-01-17 05:50:16 +08:00
世界
ccf90aee8a release: Improve publish_testflight 2026-01-17 05:50:08 +08:00
世界
e6c03fd448 Update quic-go to v0.59.0 2026-01-17 05:50:07 +08:00
世界
e0f1cdf464 platform: Uniq network interfaces 2026-01-17 05:49:57 +08:00
世界
8d88c6532f Add dial option bind_address_no_port 2026-01-17 05:49:56 +08:00
世界
3890bd2be7 platform: Display k based bytes 2026-01-17 05:49:45 +08:00
世界
6cd1eb9b94 Fix tailscale endpoint 2026-01-17 05:49:35 +08:00
世界
f196b7a583 tailscale: Add system interface support 2026-01-17 05:49:24 +08:00
世界
bd9935eebb platform: Fix gomobile build 2026-01-17 05:49:13 +08:00
世界
0e0e838ff5 platform: Update apple build comamnds 2026-01-17 05:49:13 +08:00
世界
0caebd3171 platform: Improve interface 2026-01-17 05:49:12 +08:00
世界
7d2944eba9 Downgrade quic-go to v0.57.1 2026-01-17 05:49:12 +08:00
世界
a5db2feb5e Fix linux musl builds 2026-01-17 05:48:59 +08:00
世界
708ceb3d29 Fix openwrt builds 2026-01-17 05:48:59 +08:00
世界
157e33f2a4 Add kmod-nft-queue dependency for openwrt package 2026-01-17 05:48:59 +08:00
世界
1d4fb83313 Fix nfqueue fallback 2026-01-17 05:48:58 +08:00
世界
85f5f6cebb Disable multipath TCP by default via GODEBUG 2026-01-17 05:48:58 +08:00
世界
6a750f4522 Fix missing relay support for Tailscale 2026-01-17 05:48:57 +08:00
世界
46c2cc37c3 cronet: Fix windows DNS hijack 2026-01-17 05:48:42 +08:00
世界
aa8dd6e44f Fix DNS transports 2026-01-17 05:48:41 +08:00
世界
4e94a64dcc platform: Expose process info 2026-01-17 05:48:41 +08:00
世界
494990f914 Update bypass action behavior for auto redirect 2026-01-17 05:48:41 +08:00
世界
95ccb837d3 platform: Add GetStartedAt for StartedService 2026-01-17 05:48:40 +08:00
世界
24b33a43fc documentation: Format changes header 2026-01-17 05:48:40 +08:00
世界
8ae16aa452 Add format_docs command for documentation trailing space formatting 2026-01-17 05:48:39 +08:00
世界
bf4a9edc89 Fix panic when closing Box before Start with file log output 2026-01-17 05:48:39 +08:00
世界
78b4eac974 Add pre-match support for auto redirect 2026-01-17 05:48:39 +08:00
世界
a34868468f Fix cronet on iOS 2026-01-17 05:48:38 +08:00
世界
e392c70b6f Ignore darwin IP_DONTFRAG error when not supported 2026-01-17 05:48:37 +08:00
世界
511d1bb3fa Update tailscale to v1.92.4 2026-01-17 05:48:28 +08:00
世界
4273ffa77e Update cronet-go to v143.0.7499.109-1 2026-01-17 05:48:17 +08:00
世界
f5ccf746ea platform: Split library for Android SDK 21 and 23 2026-01-17 05:48:16 +08:00
世界
b2d90b7d86 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2026-01-17 05:48:16 +08:00
世界
e0a78fde07 documentation: Minor fixes 2026-01-17 05:48:16 +08:00
世界
203f4134b0 documentation: Add Wi-Fi state shared page 2026-01-17 05:48:16 +08:00
世界
c2b697a778 Fix missing build constraints for linux wifi state monitor 2026-01-17 05:48:15 +08:00
世界
ddec2ab282 Update dependencies 2026-01-17 05:48:15 +08:00
世界
35ff7d1fb4 Update quic-go to v0.58.0 2026-01-17 05:48:04 +08:00
世界
cba18635c8 Add Chrome Root Store certificate option
Adds `chrome` as a new certificate store option alongside `mozilla`.
Both stores filter out China-based CA certificates.
2026-01-17 05:47:54 +08:00
世界
0d8c7a9c5d Fix cronet-go crash 2026-01-17 05:47:54 +08:00
世界
faff3174a3 Add trace logging for lifecycle calls
Log start/close operations with timing information for debugging.
2026-01-17 05:47:54 +08:00
世界
2fc1b672cc documentation: Minor fixes 2026-01-17 05:47:54 +08:00
世界
143983b585 Remove certificate_public_key_sha256 for naive 2026-01-17 05:47:54 +08:00
世界
4afdf4153a platform: Use new crash log api 2026-01-17 05:47:54 +08:00
世界
750dc9c3e0 Fix naive network 2026-01-17 05:47:53 +08:00
世界
48b7adde7d Add QUIC support for naiveproxy 2026-01-17 05:47:52 +08:00
世界
0585f6d065 Add ECH support for NaiveProxy outbound and tls.ech.query_server_name option
- Enable ECH for NaiveProxy outbound with DNS resolver integration
- Add query_server_name option to override domain for ECH HTTPS record queries
- Update cronet-go dependency and remove windows_386 support
2026-01-17 05:47:42 +08:00
世界
8101a7b0bd Fix naiveproxy build 2026-01-17 05:47:42 +08:00
世界
e8620587dd Add OpenAI Codex Multiplexer service 2026-01-17 05:47:42 +08:00
世界
a89680fa2d Update pricing for CCM service 2026-01-17 05:47:42 +08:00
世界
b919039c43 release: Upload only other apks 2026-01-17 05:47:42 +08:00
世界
9b0960bb5a Fix bugs and add UoT option for naiveproxy outbound 2026-01-17 05:47:41 +08:00
世界
ad7b982242 Add naiveproxy outbound 2026-01-17 05:47:41 +08:00
世界
7e68013b05 Apply ping destination filter for Windows 2026-01-17 05:47:33 +08:00
世界
ac427b98f4 platform: Add UsePlatformWIFIMonitor to gRPC interface
Align dev-next-grpc with wip2 by adding UsePlatformWIFIMonitor()
to the new PlatformInterface, allowing platform clients to indicate
they handle WIFI monitoring themselves.
2026-01-17 05:47:32 +08:00
世界
a5fb467db2 daemon: Add clear logs 2026-01-17 05:47:32 +08:00
世界
a930356b04 Revert "Stop using DHCP on iOS and tvOS" 2026-01-17 05:47:32 +08:00
世界
5bc0dfa9dd platform: Refactoring libbox to use gRPC-based protocol 2026-01-17 05:47:32 +08:00
世界
743b460e51 Add Windows WI-FI state support 2026-01-17 05:47:27 +08:00
世界
8d8ca282a1 Add Linux WI-FI state support
Support monitoring WIFI state on Linux through:
- NetworkManager (D-Bus)
- IWD (D-Bus)
- wpa_supplicant (control socket)
- ConnMan (D-Bus)
2026-01-17 05:47:04 +08:00
世界
cd56eaaba2 Add more tcp keep alive options
Also update default TCP keep-alive initial period from 10 minutes to 5 minutes.
2026-01-17 05:47:04 +08:00
世界
e92938364d Update quic-go to v0.57.1 2026-01-17 05:46:52 +08:00
世界
1c4614318e Fix read credentials for ccm service 2026-01-17 05:46:24 +08:00
世界
0f5cda4169 Add claude code multiplexer service 2026-01-17 05:46:23 +08:00
世界
d87c9fd242 Fix compatibility with MPTCP 2026-01-17 05:46:23 +08:00
世界
fce21607bd Use a more conservative strategy for resolving with systemd-resolved for local DNS server 2026-01-17 05:46:23 +08:00
世界
3dc285be8c Fix missing mTLS support in client options 2026-01-17 05:46:23 +08:00
世界
79bbce3db3 Add curve preferences, pinned public key SHA256 and mTLS for TLS options 2026-01-17 05:46:22 +08:00
世界
dfd95b2615 Fix WireGuard input packet 2026-01-17 05:46:22 +08:00
世界
ab0869c972 Update tfo-go to latest 2026-01-17 05:46:21 +08:00
世界
9ac0539ffd Remove compatibility codes 2026-01-17 05:46:14 +08:00
世界
cb4deb0c20 Do not use linkname by default to simplify debugging 2026-01-17 05:46:14 +08:00
世界
6b90b61358 documentation: Update chinese translations 2026-01-17 05:46:14 +08:00
世界
ed1ee4c3a4 Update quic-go to v0.55.0 2026-01-17 05:46:13 +08:00
世界
7f3ea8dbd8 Update WireGuard and Tailscale 2026-01-17 05:46:02 +08:00
世界
12b055989b Fix preConnectionCopy 2026-01-17 05:46:01 +08:00
世界
49056b5060 Fix ping domain 2026-01-17 05:46:01 +08:00
世界
c530995832 release: Fix linux build 2026-01-17 05:46:01 +08:00
世界
60d81a73d9 Improve ktls rx error handling 2026-01-17 05:46:01 +08:00
世界
e9c46cc359 Improve compatibility for kTLS 2026-01-17 05:46:00 +08:00
世界
9110851af3 ktls: Add warning for inappropriate scenarios 2026-01-17 05:44:43 +08:00
世界
107f92381b Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2026-01-17 05:44:42 +08:00
世界
f84129ca79 Add proxy support for ICMP echo request 2026-01-17 05:44:41 +08:00
世界
44fafcef73 Fix resolve using resolved 2026-01-17 05:44:29 +08:00
世界
a5e09fcd43 documentation: Update behavior of local DNS server on darwin 2026-01-17 05:44:29 +08:00
世界
387b42c9c2 Remove use of ldflags -checklinkname=0 on darwin 2026-01-17 05:44:29 +08:00
世界
044eb728cb Fix legacy DNS config 2026-01-17 05:44:29 +08:00
世界
2be8a45f14 Fix rule-set format 2026-01-17 05:44:29 +08:00
世界
1336987756 documentation: Remove outdated icons 2026-01-17 05:44:29 +08:00
世界
e3473d3de0 documentation: Improve local DNS server 2026-01-17 05:44:28 +08:00
世界
bba92146b1 Stop using DHCP on iOS and tvOS
We do not have the `com.apple.developer.networking.multicast` entitlement and are unable to obtain it for non-technical reasons.
2026-01-17 05:44:28 +08:00
世界
48f84b31d6 Improve local DNS server on darwin
We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does.

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

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

In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`.
2026-01-17 05:44:28 +08:00
世界
1c846df903 Use resolved in local DNS server if available 2026-01-17 05:44:28 +08:00
xchacha20-poly1305
0bd98a300f Fix rule set version 2026-01-17 05:44:27 +08:00
世界
87eaf3ce6e documentation: Add preferred_by route rule item 2026-01-17 05:44:27 +08:00
世界
239e6ec701 Add preferred_by route rule item 2026-01-17 05:44:27 +08:00
世界
5be1887f92 documentation: Add interface address rule items 2026-01-17 05:44:27 +08:00
世界
65264afdf9 Add interface address rule items 2026-01-17 05:44:26 +08:00
世界
fecdbf20de Fix ECH retry support 2026-01-17 05:44:26 +08:00
neletor
1f03080540 Add support for ech retry configs 2026-01-17 05:44:26 +08:00
Zephyruso
737162e75a Add /dns/flush-clash meta api 2026-01-17 05:44:26 +08:00
世界
51ce402dbb Bump version 2026-01-17 05:10:56 +08:00
世界
8b404b5a4c Update Go to 1.25.6 2026-01-17 05:10:56 +08:00
世界
3ce94d50dd Update uTLS to v1.8.2 2026-01-17 04:54:18 +08:00
世界
29d56fca9c Update smux to v1.5.50 & Fix h2mux RST_STREAM on half-close 2026-01-17 04:17:14 +08:00
世界
ab18010ee1 Bump version 2026-01-12 20:38:21 +08:00
世界
e69c202c79 Fix logic issues with BBR impl 2026-01-12 20:34:04 +08:00
世界
0a812f2a46 Bump version 2026-01-07 15:13:35 +08:00
Gavin Luo
fffe9fc566 Fix reset buffer in dhcp response loop
Previously, the buffer was not reset within the response loop. If a packet
handle failed or completed, the buffer retained its state. Specifically,
if `ReadPacketFrom` returned `io.ErrShortBuffer`, the error was ignored
via `continue`, but the buffer remained full. This caused the next
read attempt to immediately fail with the same error, creating a tight
busy-wait loop that consumed 100% CPU.

Validates `buffer.Reset()` is called at the start of each iteration to
ensure a clean state for 'ReadPacketFrom'.
2026-01-05 17:46:59 +08:00
世界
6fdf27a701 Fix Tailscale endpoint using wrong source IP with advertise_routes 2026-01-04 22:14:54 +08:00
Bruce Wayne
7fa7d4f0a9 ducumentation: update Shadowsocks inbound documentation for SSM API 2026-01-02 19:18:52 +08:00
世界
f511ebc1d4 Fix lint errors 2026-01-02 19:17:53 +08:00
世界
84bbdc2eba Revert "Pin gofumpt and golangci-lint versions"
This reverts commit d9d7f7880d.
2026-01-02 19:14:13 +08:00
世界
568612fc70 Fix duplicate tag detection for empty tags
Closes https://github.com/SagerNet/sing-box/issues/3665
2026-01-02 19:14:13 +08:00
世界
d78828fd81 Fix quic sniffer 2026-01-02 19:14:13 +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
306 changed files with 24620 additions and 5379 deletions

View File

@@ -14,6 +14,7 @@
--depends kmod-inet-diag --depends kmod-inet-diag
--depends kmod-tun --depends kmod-tun
--depends firewall4 --depends firewall4
--depends kmod-nft-queue
--before-remove release/config/openwrt.prerm --before-remove release/config/openwrt.prerm

23
.fpm_pacman Normal file
View File

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

View File

@@ -1 +1 @@
446096b34fb07e86541d927be26f8e5c42d41b4a ea7cd33752aed62603775af3df946c1b83f4b0b3

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

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -e -o pipefail
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
# 1.13.0 -> 1.13.0-r0
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
APK_VERSION="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
trap 'rm -rf "$ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box"
# Service files
install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service"
install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/conf.d/sing-box
/etc/init.d/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later with name use or association addition" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

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

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -e -o pipefail
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
# 1.13.0 -> 1.13.0-r0
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
APK_VERSION="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
trap 'rm -rf "$ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box"
install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/config/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
--info "provider-priority:100" \
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

View File

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

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

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.25.8"
PATCH_COMMITS=(
"afe69d3cec1c6dcf0f1797b20546795730850070"
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz"
tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
#cp -a go go_bootstrap
mv go go_osx
cd go_osx
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
# revert:
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done
# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external
# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the
# stdlib (crypto/x509) is compiled from patched src automatically.
#cd src
#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash
#cd ../..
#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz"

View File

@@ -1,16 +1,35 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.25.5" set -euo pipefail
mkdir -p $HOME/go VERSION="1.25.8"
cd $HOME/go PATCH_COMMITS=(
"466f6c7a29bc098b0d4c987b803c779222894a11"
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
"a90777dcf692dd2168577853ba743b4338721b06"
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_win7 mv go go_win7
cd go_win7 cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.25.x # these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed # that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert: # revert:
@@ -18,10 +37,10 @@ cd go_win7
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
# fixes:
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"' for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1 done
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1

View File

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

13
.github/update_cronet_dev.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 dev
git -C $PROJECTS/cronet-go fetch origin go_dev
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
go mod tidy
git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"

View File

@@ -25,8 +25,9 @@ on:
- publish-android - publish-android
push: push:
branches: branches:
- main-next - stable
- dev-next - testing
- unstable
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
@@ -40,13 +41,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }} version: ${{ steps.outputs.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.5 go-version: ~1.26.0
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -69,58 +70,61 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- { os: linux, arch: amd64, variant: purego, naive: true, openwrt: "x86_64" } - { os: linux, arch: amd64, variant: purego, naive: true }
- { os: linux, arch: amd64, variant: glibc, naive: true } - { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" } - { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } - { os: linux, arch: arm64, variant: purego, naive: true }
- { os: linux, arch: arm64, variant: glibc, naive: true } - { os: linux, arch: arm64, variant: glibc, naive: true }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } - { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: "386", go386: sse2, openwrt: "i386_pentium4" } - { os: linux, arch: "386", go386: sse2 }
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 } - { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" } - { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" }
- { os: linux, arch: arm, goarm: "7", 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, goarm: "7" }
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" } - { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" } - { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
- { os: linux, arch: riscv64, naive: true, variant: glibc }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, naive: true, variant: glibc }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" } - { os: linux, arch: arm, goarm: "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: 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, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" } - { os: linux, arch: mipsle, gomips: softfloat }
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" } - { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el } - { os: linux, arch: mips64le, gomips: hardfloat }
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" } - { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le } - { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" } - { os: linux, arch: riscv64 }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64 }
- { 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", legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" } - { os: android, arch: arm64, ndk: "aarch64-linux-android23" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" } - { os: android, arch: arm, ndk: "armv7a-linux-androideabi23" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" } - { os: android, arch: amd64, ndk: "x86_64-linux-android23" }
- { os: android, arch: "386", ndk: "i686-linux-android21" } - { os: android, arch: "386", ndk: "i686-linux-android23" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }} if: ${{ ! matrix.legacy_win7 }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.5 go-version: ~1.26.0
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.10
- name: Cache Go for Windows 7 - name: Cache Go for Windows 7
if: matrix.legacy_win7 if: matrix.legacy_win7
id: cache-go-for-windows7 id: cache-go-for-windows7
@@ -128,9 +132,11 @@ jobs:
with: with:
path: | path: |
~/go/go_win7 ~/go/go_win7
key: go_win7_1255 key: go_win7_1258
- 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'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |- run: |-
.github/setup_go_for_windows7.sh .github/setup_go_for_windows7.sh
- name: Setup Go for Windows 7 - name: Setup Go for Windows 7
@@ -154,14 +160,23 @@ jobs:
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION" git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
git -C ~/cronet-go checkout FETCH_HEAD git -C ~/cronet-go checkout FETCH_HEAD
git -C ~/cronet-go submodule update --init --recursive --depth=1 git -C ~/cronet-go submodule update --init --recursive --depth=1
- name: Regenerate Debian keyring
if: matrix.naive
run: |
set -xeuo pipefail
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
cd ~/cronet-go
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
- name: Cache Chromium toolchain - name: Cache Chromium toolchain
if: matrix.naive if: matrix.naive
id: cache-chromium-toolchain id: cache-chromium-toolchain
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts ~/cronet-go/naiveproxy/src/third_party/llvm-build/
~/cronet-go/naiveproxy/src/out/sysroot-build ~/cronet-go/naiveproxy/src/gn/out/
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
~/cronet-go/naiveproxy/src/out/sysroot-build/
key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }} key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
- name: Download Chromium toolchain - name: Download Chromium toolchain
if: matrix.naive if: matrix.naive
@@ -190,9 +205,10 @@ 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,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
if [[ "${{ matrix.naive }}" == "true" ]]; then if [[ "${{ matrix.naive }}" == "true" ]]; then
TAGS="${TAGS},with_naive_outbound" TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
fi fi
if [[ "${{ matrix.variant }}" == "purego" ]]; then if [[ "${{ matrix.variant }}" == "purego" ]]; then
TAGS="${TAGS},with_purego" TAGS="${TAGS},with_purego"
@@ -200,13 +216,16 @@ jobs:
TAGS="${TAGS},with_musl" TAGS="${TAGS},with_musl"
fi fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build (purego) - name: Build (purego)
if: matrix.variant == 'purego' if: matrix.variant == 'purego'
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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' \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -228,7 +247,7 @@ jobs:
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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' \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "1" CGO_ENABLED: "1"
@@ -236,6 +255,8 @@ jobs:
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }} GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }} GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (musl) - name: Build (musl)
if: matrix.variant == 'musl' if: matrix.variant == 'musl'
@@ -243,7 +264,7 @@ jobs:
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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' \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "1" CGO_ENABLED: "1"
@@ -251,6 +272,8 @@ jobs:
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }} GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }} GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build (non-variant) - name: Build (non-variant)
if: matrix.os != 'android' && matrix.variant == '' if: matrix.os != 'android' && matrix.variant == ''
@@ -258,7 +281,7 @@ jobs:
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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' \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -278,7 +301,7 @@ jobs:
export CXX="${CC}++" export CXX="${CC}++"
mkdir -p dist mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build 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' \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "1" CGO_ENABLED: "1"
@@ -352,7 +375,7 @@ jobs:
sudo gem install fpm sudo gem install fpm
sudo apt-get update sudo apt-get update
sudo apt-get install -y libarchive-tools sudo apt-get install -y libarchive-tools
cp .fpm_systemd .fpm cp .fpm_pacman .fpm
fpm -t pacman \ fpm -t pacman \
-v "$PKG_VERSION" \ -v "$PKG_VERSION" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \ -p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
@@ -369,14 +392,34 @@ 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}${SUFFIX}.ipk" .github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done done
rm "dist/openwrt.deb" rm "dist/openwrt.deb"
- name: Install apk-tools
if: matrix.openwrt != '' || matrix.alpine != ''
run: |-
docker run --rm -v /usr/local/bin:/mnt alpine:edge sh -c "apk add --no-cache apk-tools-static && cp /sbin/apk.static /mnt/apk && chmod +x /mnt/apk"
- name: Package OpenWrt APK
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
for architecture in ${{ matrix.openwrt }}; do
.github/build_openwrt_apk.sh \
"$architecture" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.apk"
done
- name: Package Alpine APK
if: matrix.alpine != ''
run: |-
set -xeuo pipefail
.github/build_alpine_apk.sh \
"${{ matrix.alpine }}" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.alpine }}.apk"
- name: Archive - name: Archive
run: | run: |
set -xeuo pipefail set -xeuo pipefail
@@ -412,22 +455,36 @@ jobs:
include: include:
- { arch: amd64 } - { arch: amd64 }
- { arch: arm64 } - { arch: arm64 }
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" } - { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! matrix.legacy_go124 }} if: ${{ ! matrix.legacy_osx }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.25.3
- name: Setup Go 1.24 - name: Cache Go for macOS 10.13
if: matrix.legacy_go124 if: matrix.legacy_osx
uses: actions/setup-go@v5 id: cache-go-for-macos1013
uses: actions/cache@v4
with: with:
go-version: ~1.24.6 path: |
~/go/go_osx
key: go_osx_1258
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |-
.github/setup_go_for_macos1013.sh
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx
run: |-
echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV
- name: Set tag - 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"
@@ -435,22 +492,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,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0' if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then TAGS=$(cat release/DEFAULT_BUILD_TAGS)
TAGS="${TAGS},with_naive_outbound" else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
fi fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
run: |
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
- name: Build - name: Build
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ 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' \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "1" CGO_ENABLED: "1"
GOOS: darwin GOOS: darwin
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name - name: Set name
run: |- run: |-
@@ -489,7 +551,7 @@ jobs:
- { arch: arm64, naive: true } - { arch: arm64, naive: true }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -503,9 +565,11 @@ jobs:
- name: Build - name: Build
if: matrix.naive if: matrix.naive
run: | run: |
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_WINDOWS
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
mkdir -p dist 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" ` go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0" ` -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -515,9 +579,11 @@ jobs:
- name: Build - name: Build
if: ${{ !matrix.naive }} if: ${{ !matrix.naive }}
run: | run: |
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_OTHERS
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
mkdir -p dist 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" ` go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0" ` -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -562,20 +628,20 @@ jobs:
path: "dist" path: "dist"
build_android: build_android:
name: Build Android name: Build Android
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android' if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- calculate_version - calculate_version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.5 go-version: ~1.26.0
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -598,12 +664,12 @@ jobs:
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 }}
- name: Checkout main branch - name: Checkout main branch
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch' if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
run: |- run: |-
cd clients/android cd clients/android
git checkout main git checkout main
- name: Checkout dev branch - name: Checkout dev branch
if: github.ref == 'refs/heads/dev-next' if: github.ref == 'refs/heads/testing'
run: |- run: |-
cd clients/android cd clients/android
git checkout dev git checkout dev
@@ -623,9 +689,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 }}
@@ -635,6 +701,7 @@ jobs:
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/*.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_CODE=$(grep VERSION_CODE clients/android/version.properties | cut -d= -f2)
VERSION_NAME=$(grep VERSION_NAME 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 cat > dist/SFA-version-metadata.json << EOF
@@ -651,20 +718,20 @@ jobs:
path: 'dist' path: 'dist'
publish_android: publish_android:
name: Publish Android name: Publish Android
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- calculate_version - calculate_version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.5 go-version: ~1.26.0
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -687,12 +754,12 @@ jobs:
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 }}
- name: Checkout main branch - name: Checkout main branch
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch' if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
run: |- run: |-
cd clients/android cd clients/android
git checkout main git checkout main
- name: Checkout dev branch - name: Checkout dev branch
if: github.ref == 'refs/heads/dev-next' if: github.ref == 'refs/heads/testing'
run: |- run: |-
cd clients/android cd clients/android
git checkout dev git checkout dev
@@ -705,7 +772,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
@@ -755,7 +822,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
if: matrix.if if: matrix.if
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
@@ -763,7 +830,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.5 go-version: ~1.26.0
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-
@@ -771,12 +838,12 @@ jobs:
git tag v${{ needs.calculate_version.outputs.version }} -f git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Checkout main branch - name: Checkout main branch
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch' if: matrix.if && github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
run: |- run: |-
cd clients/apple cd clients/apple
git checkout main git checkout main
- name: Checkout dev branch - name: Checkout dev branch
if: matrix.if && github.ref == 'refs/heads/dev-next' if: matrix.if && github.ref == 'refs/heads/testing'
run: |- run: |-
cd clients/apple cd clients/apple
git checkout dev git checkout dev
@@ -862,7 +929,7 @@ jobs:
-authenticationKeyID $ASC_KEY_ID \ -authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
- name: Publish to TestFlight - name: Publish to TestFlight
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next' if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing'
run: |- run: |-
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }} go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
- name: Build image - name: Build image
@@ -909,7 +976,7 @@ jobs:
- build_apple - build_apple
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Cache ghr - name: Cache ghr

View File

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

View File

@@ -3,18 +3,20 @@ name: Lint
on: on:
push: push:
branches: branches:
- stable-next - oldstable
- main-next - stable
- dev-next - testing
- unstable
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- '.github/**' - '.github/**'
- '!.github/workflows/lint.yml' - '!.github/workflows/lint.yml'
pull_request: pull_request:
branches: branches:
- stable-next - oldstable
- main-next - stable
- dev-next - testing
- unstable
jobs: jobs:
build: build:
@@ -22,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
@@ -32,7 +34,7 @@ jobs:
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v8
with: with:
version: v2.4.0 version: latest
args: --timeout=30m args: --timeout=30m
install-mode: binary install-mode: binary
verify: false verify: false

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

@@ -12,10 +12,11 @@ RUN set -ex \
&& apk add git build-base \ && apk add git build-base \
&& 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 \ && export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" \ && export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
&& go build -v -trimpath -tags "$TAGS" \
-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\" $LDFLAGS_SHARED -s -w -buildid=" \
./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>"

View File

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

120
Makefile
View File

@@ -1,15 +1,18 @@
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,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0 TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS)
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0" LDFLAGS_SHARED = $(shell cat release/LDFLAGS)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH) PREFIX ?= $(shell go env GOPATH)
SING_FFI ?= sing-ffi
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
.PHONY: test release docs build .PHONY: test release docs build
@@ -37,8 +40,11 @@ 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@latest
go install -v github.com/daixiang0/gci@latest go install -v github.com/daixiang0/gci@latest
lint: lint:
@@ -49,7 +55,7 @@ lint:
GOOS=freebsd golangci-lint run ./... GOOS=freebsd golangci-lint run ./...
lint_install: lint_install:
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
proto: proto:
@go run ./cmd/internal/protogen @go run ./cmd/internal/protogen
@@ -86,12 +92,12 @@ update_android_version:
go run ./cmd/internal/update_android_version go run ./cmd/internal/update_android_version
build_android: build_android:
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop
upload_android: upload_android:
mkdir -p dist/release_android mkdir -p dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android rm -rf dist/release_android
@@ -106,7 +112,7 @@ build_ios:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \ rm -rf build/SFI.xcarchive && \
xcodebuild clean -scheme SFI && \ xcodebuild clean -scheme SFI && \
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
upload_ios_app_store: upload_ios_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -127,7 +133,7 @@ release_ios: build_ios upload_ios_app_store
build_macos: build_macos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFM.xcarchive && \ rm -rf build/SFM.xcarchive && \
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
upload_macos_app_store: upload_macos_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -136,54 +142,50 @@ upload_macos_app_store:
release_macos: build_macos upload_macos_app_store release_macos: build_macos upload_macos_app_store
build_macos_standalone: build_macos_standalone:
cd ../sing-box-for-apple && \ $(MAKE) -C ../sing-box-for-apple archive_macos_standalone
rm -rf build/SFM.System.xcarchive && \
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
build_macos_dmg: build_macos_dmg:
rm -rf dist/SFM $(MAKE) -C ../sing-box-for-apple build_macos_dmg
mkdir -p dist/SFM
cd ../sing-box-for-apple && \ build_macos_pkg:
rm -rf build/SFM.System && \ $(MAKE) -C ../sing-box-for-apple build_macos_pkg
rm -rf build/SFM.dmg && \
xcodebuild -exportArchive \
-archivePath "build/SFM.System.xcarchive" \
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
-exportPath "build/SFM.System" && \
create-dmg \
--volname "sing-box" \
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
--icon "SFM.app" 0 0 \
--hide-extension "SFM.app" \
--app-drop-link 0 0 \
--skip-jenkins \
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
notarize_macos_dmg: notarize_macos_dmg:
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \ $(MAKE) -C ../sing-box-for-apple notarize_macos_dmg
--keychain-profile "notarytool-password" \
--no-s3-acceleration notarize_macos_pkg:
$(MAKE) -C ../sing-box-for-apple notarize_macos_pkg
upload_macos_dmg: upload_macos_dmg:
cd dist/SFM && \ mkdir -p dist/SFM
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \ cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg" cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg"
cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg"
upload_macos_pkg:
mkdir -p dist/SFM
cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg"
cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg"
cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
upload_macos_dsyms: upload_macos_dsyms:
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \ mkdir -p dist/SFM
zip -r SFM.dSYMs.zip dSYMs && \ cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \ cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
popd && \ ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
cd dist/SFM && \
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
build_tvos: build_tvos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFT.xcarchive && \ rm -rf build/SFT.xcarchive && \
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
upload_tvos_app_store: upload_tvos_app_store:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -207,12 +209,12 @@ update_apple_version:
update_macos_version: update_macos_version:
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple_beta: update_apple_version release_ios release_macos release_tvos release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight: publish_testflight:
go run -v ./cmd/internal/app_store_connect publish_testflight go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
prepare_app_store: prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store go run -v ./cmd/internal/app_store_connect prepare_app_store
@@ -235,22 +237,21 @@ test_stdio:
lib_android: lib_android:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox -target android
lib_android_debug:
go run ./cmd/internal/build_libbox -target android -debug
lib_apple: lib_apple:
go run ./cmd/internal/build_libbox -target apple go run ./cmd/internal/build_libbox -target apple
lib_ios: lib_windows:
go run ./cmd/internal/build_libbox -target apple -platform ios -debug $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
lib: lib_android_new:
go run ./cmd/internal/build_libbox -target android $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
go run ./cmd/internal/build_libbox -target ios
lib_apple_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.10 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.10 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve
@@ -259,8 +260,8 @@ publish_docs:
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install: docs_install:
python -m venv venv python3 -m venv venv
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*" source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*"
clean: clean:
rm -rf bin dist sing-box rm -rf bin dist sing-box
@@ -270,3 +271,6 @@ update:
git fetch git fetch
git reset FETCH_HEAD --hard git reset FETCH_HEAD --hard
git clean -fdx git clean -fdx
%:
@:

View File

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

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

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

View File

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

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
} }

23
adapter/neighbor.go Normal file
View File

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

View File

@@ -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

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

View File

@@ -21,10 +21,13 @@ 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
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker) AppendTracker(tracker ConnectionTracker)
ResetNetwork() ResetNetwork()
} }

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 {

54
box.go
View File

@@ -125,7 +125,10 @@ func New(options Options) (*Box, error) {
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
if err != nil {
return nil, err
}
var needCacheFile bool var needCacheFile bool
var needClashAPI bool var needClashAPI bool
var needV2RayAPI bool var needV2RayAPI bool
@@ -443,15 +446,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 +466,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 +500,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

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

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"
@@ -16,17 +17,17 @@ import (
) )
var ( var (
debugEnabled bool debugEnabled bool
target string target string
platform string platform string
withTailscale bool // withTailscale bool
) )
func init() { func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug") flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform") flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform") flag.StringVar(&platform, "platform", "", "specify platform")
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS") // flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
} }
func main() { func main() {
@@ -47,7 +48,7 @@ var (
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
darwinTags []string darwinTags []string
memcTags []string // memcTags []string
notMemcTags []string notMemcTags []string
debugTags []string debugTags []string
) )
@@ -59,19 +60,38 @@ func init() {
if err != nil { if err != nil {
currentTag = "unknown" currentTag = "unknown"
} }
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+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -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+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
darwinTags = append(darwinTags, "with_dhcp") darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
memcTags = append(memcTags, "with_tailscale") // memcTags = append(memcTags, "with_tailscale")
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
notMemcTags = append(notMemcTags, "with_low_memory") 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 +107,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 +135,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 != "" {
@@ -147,7 +195,7 @@ func buildApple() {
} else if debugEnabled { } else if debugEnabled {
bindTarget = "ios" bindTarget = "ios"
} else { } else {
bindTarget = "ios,tvos,macos" bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
} }
args := []string{ args := []string{
@@ -157,9 +205,9 @@ func buildApple() {
"-libname=box", "-libname=box",
"-tags-not-macos=with_low_memory", "-tags-not-macos=with_low_memory",
} }
if !withTailscale { //if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(memcTags, ",")) // args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
} //}
if !debugEnabled { if !debugEnabled {
args = append(args, sharedFlags...) args = append(args, sharedFlags...)
@@ -168,9 +216,9 @@ func buildApple() {
} }
tags := append(sharedTags, darwinTags...) tags := append(sharedTags, darwinTags...)
if withTailscale { //if withTailscale {
tags = append(tags, memcTags...) // tags = append(tags, memcTags...)
} //}
if debugEnabled { if debugEnabled {
tags = append(tags, debugTags...) tags = append(tags, debugTags...)
} }

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings package settings
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings package settings
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build linux
package settings package settings
import ( import (

View File

@@ -303,8 +303,6 @@ find:
metadata.Protocol = C.ProtocolQUIC metadata.Protocol = C.ProtocolQUIC
fingerprint, err := ja3.Compute(buffer.Bytes()) fingerprint, err := ja3.Compute(buffer.Bytes())
if err != nil { if err != nil {
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments metadata.SniffContext = fragments
return E.Cause1(ErrNeedMoreData, err) return E.Cause1(ErrNeedMoreData, err)
} }
@@ -334,7 +332,7 @@ find:
} }
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 { if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
if maybeUQUIC(fingerprint) { if isQUICGo(fingerprint) {
metadata.Client = C.ClientQUICGo metadata.Client = C.ClientQUICGo
} else { } else {
metadata.Client = C.ClientChromium metadata.Client = C.ClientChromium

View File

@@ -1,21 +1,29 @@
package sniff package sniff
import ( import (
"crypto/tls"
"github.com/sagernet/sing-box/common/ja3" "github.com/sagernet/sing-box/common/ja3"
) )
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior const (
// The cronet without this behavior does not have version 115 // X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
var uQUICChrome115 = &ja3.ClientHello{ x25519Kyber768Draft00 uint16 = 0x11EC // 4588
Version: tls.VersionTLS12, // renegotiation_info extension used by Go crypto/tls
CipherSuites: []uint16{4865, 4866, 4867}, extensionRenegotiationInfo uint16 = 0xFF01 // 65281
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513}, )
EllipticCurves: []uint16{29, 23, 24},
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
}
func maybeUQUIC(fingerprint *ja3.ClientHello) bool { // isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
return !uQUICChrome115.Equals(fingerprint, true) // Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
func isQUICGo(fingerprint *ja3.ClientHello) bool {
for _, curve := range fingerprint.EllipticCurves {
if curve == x25519Kyber768Draft00 {
return true
}
}
for _, ext := range fingerprint.Extensions {
if ext == extensionRenegotiationInfo {
return true
}
}
return false
} }

View File

@@ -0,0 +1,188 @@
package sniff_test
import (
"context"
"crypto/tls"
"encoding/hex"
"errors"
"net"
"testing"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require"
)
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
t.Parallel()
const testSNI = "test.example.com"
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer udpConn.Close()
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
packetsChan := make(chan [][]byte, 1)
go func() {
var packets [][]byte
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
for i := 0; i < 10; i++ {
buf := make([]byte, 2048)
n, _, err := udpConn.ReadFromUDP(buf)
if err != nil {
break
}
packets = append(packets, buf[:n])
}
packetsChan <- packets
}()
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer clientConn.Close()
tlsConfig := &tls.Config{
ServerName: testSNI,
InsecureSkipVerify: true,
NextProtos: []string{"h3"},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
select {
case packets := <-packetsChan:
t.Logf("Captured %d packets", len(packets))
var metadata adapter.InboundContext
for i, pkt := range packets {
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
if metadata.Domain != "" {
break
}
}
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
t.Logf("Domain: %s", metadata.Domain)
t.Logf("Client: %s", metadata.Client)
t.Logf("Protocol: %s", metadata.Protocol)
// The client should be identified as quic-go, not chromium
// Current issue: it's being identified as chromium
if metadata.Client == "chromium" {
t.Log("WARNING: quic-go is being misidentified as chromium!")
}
case <-time.After(5 * time.Second):
t.Fatal("Timeout")
}
}
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
t.Parallel()
const testSNI = "test.example.com"
// Create UDP listener to capture ALL initial packets
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer udpConn.Close()
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
// Channel to receive captured packets
packetsChan := make(chan [][]byte, 1)
// Start goroutine to capture packets
go func() {
var packets [][]byte
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
for i := 0; i < 5; i++ { // Capture up to 5 packets
buf := make([]byte, 2048)
n, _, err := udpConn.ReadFromUDP(buf)
if err != nil {
break
}
packets = append(packets, buf[:n])
}
packetsChan <- packets
}()
// Create QUIC client connection (will fail but we capture the initial packet)
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer clientConn.Close()
tlsConfig := &tls.Config{
ServerName: testSNI,
InsecureSkipVerify: true,
NextProtos: []string{"h3"},
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// This will fail (no server) but sends initial packet
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
// Wait for captured packets
select {
case packets := <-packetsChan:
t.Logf("Captured %d QUIC packets", len(packets))
for i, packet := range packets {
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
}
// Test sniffer with first packet
if len(packets) > 0 {
var metadata adapter.InboundContext
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
t.Logf("First packet sniff error: %v", err)
t.Logf("Protocol: %s", metadata.Protocol)
t.Logf("Domain: %s", metadata.Domain)
t.Logf("Client: %s", metadata.Client)
// If first packet needs more data, try with subsequent packets
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
t.Log("First packet needs more data, trying subsequent packets with shared context...")
for i := 1; i < len(packets); i++ {
// Reuse same metadata to accumulate fragments
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
break
}
}
}
// Print hex dump for debugging
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
// Log final results
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
// Verify SNI extraction
if metadata.Domain == "" {
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
} else {
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
}
// Check client identification - quic-go should be identified as quic-go, not chromium
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
}
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for QUIC packets")
}
}

View File

@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
var metadata adapter.InboundContext var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium) require.Empty(t, metadata.Client)
require.ErrorIs(t, err, sniff.ErrNeedMoreData) require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894") pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err) require.NoError(t, err)
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
var metadata adapter.InboundContext var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium) require.Empty(t, metadata.Client)
require.ErrorIs(t, err, sniff.ErrNeedMoreData) require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err) require.NoError(t, err)

View File

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

494
common/srs/compat_test.go Normal file
View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/acmedns"
"github.com/libdns/alidns" "github.com/libdns/alidns"
"github.com/libdns/cloudflare" "github.com/libdns/cloudflare"
"github.com/mholt/acmez/v3/acme" "github.com/mholt/acmez/v3/acme"
@@ -114,13 +115,24 @@ 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,
}
case C.DNSProviderACMEDNS:
solver.DNSProvider = &acmedns.Provider{
Username: dnsOptions.ACMEDNSOptions.Username,
Password: dnsOptions.ACMEDNSOptions.Password,
Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
} }
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

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

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

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

View File

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

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

@@ -1,702 +0,0 @@
package daemon
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type SubscribeHelperRequestRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AcceptGetWIFIStateRequests bool `protobuf:"varint,1,opt,name=acceptGetWIFIStateRequests,proto3" json:"acceptGetWIFIStateRequests,omitempty"`
AcceptFindConnectionOwnerRequests bool `protobuf:"varint,2,opt,name=acceptFindConnectionOwnerRequests,proto3" json:"acceptFindConnectionOwnerRequests,omitempty"`
AcceptSendNotificationRequests bool `protobuf:"varint,3,opt,name=acceptSendNotificationRequests,proto3" json:"acceptSendNotificationRequests,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SubscribeHelperRequestRequest) Reset() {
*x = SubscribeHelperRequestRequest{}
mi := &file_daemon_helper_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SubscribeHelperRequestRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubscribeHelperRequestRequest) ProtoMessage() {}
func (x *SubscribeHelperRequestRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[0]
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 SubscribeHelperRequestRequest.ProtoReflect.Descriptor instead.
func (*SubscribeHelperRequestRequest) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{0}
}
func (x *SubscribeHelperRequestRequest) GetAcceptGetWIFIStateRequests() bool {
if x != nil {
return x.AcceptGetWIFIStateRequests
}
return false
}
func (x *SubscribeHelperRequestRequest) GetAcceptFindConnectionOwnerRequests() bool {
if x != nil {
return x.AcceptFindConnectionOwnerRequests
}
return false
}
func (x *SubscribeHelperRequestRequest) GetAcceptSendNotificationRequests() bool {
if x != nil {
return x.AcceptSendNotificationRequests
}
return false
}
type HelperRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
// Types that are valid to be assigned to Request:
//
// *HelperRequest_GetWIFIState
// *HelperRequest_FindConnectionOwner
// *HelperRequest_SendNotification
Request isHelperRequest_Request `protobuf_oneof:"request"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HelperRequest) Reset() {
*x = HelperRequest{}
mi := &file_daemon_helper_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HelperRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelperRequest) ProtoMessage() {}
func (x *HelperRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[1]
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 HelperRequest.ProtoReflect.Descriptor instead.
func (*HelperRequest) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{1}
}
func (x *HelperRequest) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *HelperRequest) GetRequest() isHelperRequest_Request {
if x != nil {
return x.Request
}
return nil
}
func (x *HelperRequest) GetGetWIFIState() *emptypb.Empty {
if x != nil {
if x, ok := x.Request.(*HelperRequest_GetWIFIState); ok {
return x.GetWIFIState
}
}
return nil
}
func (x *HelperRequest) GetFindConnectionOwner() *FindConnectionOwnerRequest {
if x != nil {
if x, ok := x.Request.(*HelperRequest_FindConnectionOwner); ok {
return x.FindConnectionOwner
}
}
return nil
}
func (x *HelperRequest) GetSendNotification() *Notification {
if x != nil {
if x, ok := x.Request.(*HelperRequest_SendNotification); ok {
return x.SendNotification
}
}
return nil
}
type isHelperRequest_Request interface {
isHelperRequest_Request()
}
type HelperRequest_GetWIFIState struct {
GetWIFIState *emptypb.Empty `protobuf:"bytes,2,opt,name=getWIFIState,proto3,oneof"`
}
type HelperRequest_FindConnectionOwner struct {
FindConnectionOwner *FindConnectionOwnerRequest `protobuf:"bytes,3,opt,name=findConnectionOwner,proto3,oneof"`
}
type HelperRequest_SendNotification struct {
SendNotification *Notification `protobuf:"bytes,4,opt,name=sendNotification,proto3,oneof"`
}
func (*HelperRequest_GetWIFIState) isHelperRequest_Request() {}
func (*HelperRequest_FindConnectionOwner) isHelperRequest_Request() {}
func (*HelperRequest_SendNotification) isHelperRequest_Request() {}
type FindConnectionOwnerRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
IpProtocol int32 `protobuf:"varint,1,opt,name=ipProtocol,proto3" json:"ipProtocol,omitempty"`
SourceAddress string `protobuf:"bytes,2,opt,name=sourceAddress,proto3" json:"sourceAddress,omitempty"`
SourcePort int32 `protobuf:"varint,3,opt,name=sourcePort,proto3" json:"sourcePort,omitempty"`
DestinationAddress string `protobuf:"bytes,4,opt,name=destinationAddress,proto3" json:"destinationAddress,omitempty"`
DestinationPort int32 `protobuf:"varint,5,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FindConnectionOwnerRequest) Reset() {
*x = FindConnectionOwnerRequest{}
mi := &file_daemon_helper_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FindConnectionOwnerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FindConnectionOwnerRequest) ProtoMessage() {}
func (x *FindConnectionOwnerRequest) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[2]
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 FindConnectionOwnerRequest.ProtoReflect.Descriptor instead.
func (*FindConnectionOwnerRequest) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{2}
}
func (x *FindConnectionOwnerRequest) GetIpProtocol() int32 {
if x != nil {
return x.IpProtocol
}
return 0
}
func (x *FindConnectionOwnerRequest) GetSourceAddress() string {
if x != nil {
return x.SourceAddress
}
return ""
}
func (x *FindConnectionOwnerRequest) GetSourcePort() int32 {
if x != nil {
return x.SourcePort
}
return 0
}
func (x *FindConnectionOwnerRequest) GetDestinationAddress() string {
if x != nil {
return x.DestinationAddress
}
return ""
}
func (x *FindConnectionOwnerRequest) GetDestinationPort() int32 {
if x != nil {
return x.DestinationPort
}
return 0
}
type Notification struct {
state protoimpl.MessageState `protogen:"open.v1"`
Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
TypeName string `protobuf:"bytes,2,opt,name=typeName,proto3" json:"typeName,omitempty"`
TypeId int32 `protobuf:"varint,3,opt,name=typeId,proto3" json:"typeId,omitempty"`
Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"`
Subtitle string `protobuf:"bytes,5,opt,name=subtitle,proto3" json:"subtitle,omitempty"`
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
OpenURL string `protobuf:"bytes,7,opt,name=openURL,proto3" json:"openURL,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Notification) Reset() {
*x = Notification{}
mi := &file_daemon_helper_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Notification) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Notification) ProtoMessage() {}
func (x *Notification) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[3]
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 Notification.ProtoReflect.Descriptor instead.
func (*Notification) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{3}
}
func (x *Notification) GetIdentifier() string {
if x != nil {
return x.Identifier
}
return ""
}
func (x *Notification) GetTypeName() string {
if x != nil {
return x.TypeName
}
return ""
}
func (x *Notification) GetTypeId() int32 {
if x != nil {
return x.TypeId
}
return 0
}
func (x *Notification) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *Notification) GetSubtitle() string {
if x != nil {
return x.Subtitle
}
return ""
}
func (x *Notification) GetBody() string {
if x != nil {
return x.Body
}
return ""
}
func (x *Notification) GetOpenURL() string {
if x != nil {
return x.OpenURL
}
return ""
}
type HelperResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
// Types that are valid to be assigned to Response:
//
// *HelperResponse_WifiState
// *HelperResponse_Error
// *HelperResponse_ConnectionOwner
Response isHelperResponse_Response `protobuf_oneof:"response"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HelperResponse) Reset() {
*x = HelperResponse{}
mi := &file_daemon_helper_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HelperResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelperResponse) ProtoMessage() {}
func (x *HelperResponse) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[4]
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 HelperResponse.ProtoReflect.Descriptor instead.
func (*HelperResponse) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{4}
}
func (x *HelperResponse) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *HelperResponse) GetResponse() isHelperResponse_Response {
if x != nil {
return x.Response
}
return nil
}
func (x *HelperResponse) GetWifiState() *WIFIState {
if x != nil {
if x, ok := x.Response.(*HelperResponse_WifiState); ok {
return x.WifiState
}
}
return nil
}
func (x *HelperResponse) GetError() string {
if x != nil {
if x, ok := x.Response.(*HelperResponse_Error); ok {
return x.Error
}
}
return ""
}
func (x *HelperResponse) GetConnectionOwner() *ConnectionOwner {
if x != nil {
if x, ok := x.Response.(*HelperResponse_ConnectionOwner); ok {
return x.ConnectionOwner
}
}
return nil
}
type isHelperResponse_Response interface {
isHelperResponse_Response()
}
type HelperResponse_WifiState struct {
WifiState *WIFIState `protobuf:"bytes,2,opt,name=wifiState,proto3,oneof"`
}
type HelperResponse_Error struct {
Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
}
type HelperResponse_ConnectionOwner struct {
ConnectionOwner *ConnectionOwner `protobuf:"bytes,4,opt,name=connectionOwner,proto3,oneof"`
}
func (*HelperResponse_WifiState) isHelperResponse_Response() {}
func (*HelperResponse_Error) isHelperResponse_Response() {}
func (*HelperResponse_ConnectionOwner) isHelperResponse_Response() {}
type ConnectionOwner struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int32 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
UserName string `protobuf:"bytes,2,opt,name=userName,proto3" json:"userName,omitempty"`
ProcessPath string `protobuf:"bytes,3,opt,name=processPath,proto3" json:"processPath,omitempty"`
AndroidPackageName string `protobuf:"bytes,4,opt,name=androidPackageName,proto3" json:"androidPackageName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnectionOwner) Reset() {
*x = ConnectionOwner{}
mi := &file_daemon_helper_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnectionOwner) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnectionOwner) ProtoMessage() {}
func (x *ConnectionOwner) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[5]
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 ConnectionOwner.ProtoReflect.Descriptor instead.
func (*ConnectionOwner) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{5}
}
func (x *ConnectionOwner) GetUserId() int32 {
if x != nil {
return x.UserId
}
return 0
}
func (x *ConnectionOwner) GetUserName() string {
if x != nil {
return x.UserName
}
return ""
}
func (x *ConnectionOwner) GetProcessPath() string {
if x != nil {
return x.ProcessPath
}
return ""
}
func (x *ConnectionOwner) GetAndroidPackageName() string {
if x != nil {
return x.AndroidPackageName
}
return ""
}
type WIFIState struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ssid string `protobuf:"bytes,1,opt,name=ssid,proto3" json:"ssid,omitempty"`
Bssid string `protobuf:"bytes,2,opt,name=bssid,proto3" json:"bssid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WIFIState) Reset() {
*x = WIFIState{}
mi := &file_daemon_helper_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *WIFIState) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WIFIState) ProtoMessage() {}
func (x *WIFIState) ProtoReflect() protoreflect.Message {
mi := &file_daemon_helper_proto_msgTypes[6]
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 WIFIState.ProtoReflect.Descriptor instead.
func (*WIFIState) Descriptor() ([]byte, []int) {
return file_daemon_helper_proto_rawDescGZIP(), []int{6}
}
func (x *WIFIState) GetSsid() string {
if x != nil {
return x.Ssid
}
return ""
}
func (x *WIFIState) GetBssid() string {
if x != nil {
return x.Bssid
}
return ""
}
var File_daemon_helper_proto protoreflect.FileDescriptor
const file_daemon_helper_proto_rawDesc = "" +
"\n" +
"\x13daemon/helper.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xf5\x01\n" +
"\x1dSubscribeHelperRequestRequest\x12>\n" +
"\x1aacceptGetWIFIStateRequests\x18\x01 \x01(\bR\x1aacceptGetWIFIStateRequests\x12L\n" +
"!acceptFindConnectionOwnerRequests\x18\x02 \x01(\bR!acceptFindConnectionOwnerRequests\x12F\n" +
"\x1eacceptSendNotificationRequests\x18\x03 \x01(\bR\x1eacceptSendNotificationRequests\"\x84\x02\n" +
"\rHelperRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12<\n" +
"\fgetWIFIState\x18\x02 \x01(\v2\x16.google.protobuf.EmptyH\x00R\fgetWIFIState\x12V\n" +
"\x13findConnectionOwner\x18\x03 \x01(\v2\".daemon.FindConnectionOwnerRequestH\x00R\x13findConnectionOwner\x12B\n" +
"\x10sendNotification\x18\x04 \x01(\v2\x14.daemon.NotificationH\x00R\x10sendNotificationB\t\n" +
"\arequest\"\xdc\x01\n" +
"\x1aFindConnectionOwnerRequest\x12\x1e\n" +
"\n" +
"ipProtocol\x18\x01 \x01(\x05R\n" +
"ipProtocol\x12$\n" +
"\rsourceAddress\x18\x02 \x01(\tR\rsourceAddress\x12\x1e\n" +
"\n" +
"sourcePort\x18\x03 \x01(\x05R\n" +
"sourcePort\x12.\n" +
"\x12destinationAddress\x18\x04 \x01(\tR\x12destinationAddress\x12(\n" +
"\x0fdestinationPort\x18\x05 \x01(\x05R\x0fdestinationPort\"\xc2\x01\n" +
"\fNotification\x12\x1e\n" +
"\n" +
"identifier\x18\x01 \x01(\tR\n" +
"identifier\x12\x1a\n" +
"\btypeName\x18\x02 \x01(\tR\btypeName\x12\x16\n" +
"\x06typeId\x18\x03 \x01(\x05R\x06typeId\x12\x14\n" +
"\x05title\x18\x04 \x01(\tR\x05title\x12\x1a\n" +
"\bsubtitle\x18\x05 \x01(\tR\bsubtitle\x12\x12\n" +
"\x04body\x18\x06 \x01(\tR\x04body\x12\x18\n" +
"\aopenURL\x18\a \x01(\tR\aopenURL\"\xbc\x01\n" +
"\x0eHelperResponse\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x121\n" +
"\twifiState\x18\x02 \x01(\v2\x11.daemon.WIFIStateH\x00R\twifiState\x12\x16\n" +
"\x05error\x18\x03 \x01(\tH\x00R\x05error\x12C\n" +
"\x0fconnectionOwner\x18\x04 \x01(\v2\x17.daemon.ConnectionOwnerH\x00R\x0fconnectionOwnerB\n" +
"\n" +
"\bresponse\"\x97\x01\n" +
"\x0fConnectionOwner\x12\x16\n" +
"\x06userId\x18\x01 \x01(\x05R\x06userId\x12\x1a\n" +
"\buserName\x18\x02 \x01(\tR\buserName\x12 \n" +
"\vprocessPath\x18\x03 \x01(\tR\vprocessPath\x12.\n" +
"\x12androidPackageName\x18\x04 \x01(\tR\x12androidPackageName\"5\n" +
"\tWIFIState\x12\x12\n" +
"\x04ssid\x18\x01 \x01(\tR\x04ssid\x12\x14\n" +
"\x05bssid\x18\x02 \x01(\tR\x05bssidB%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
var (
file_daemon_helper_proto_rawDescOnce sync.Once
file_daemon_helper_proto_rawDescData []byte
)
func file_daemon_helper_proto_rawDescGZIP() []byte {
file_daemon_helper_proto_rawDescOnce.Do(func() {
file_daemon_helper_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)))
})
return file_daemon_helper_proto_rawDescData
}
var (
file_daemon_helper_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
file_daemon_helper_proto_goTypes = []any{
(*SubscribeHelperRequestRequest)(nil), // 0: daemon.SubscribeHelperRequestRequest
(*HelperRequest)(nil), // 1: daemon.HelperRequest
(*FindConnectionOwnerRequest)(nil), // 2: daemon.FindConnectionOwnerRequest
(*Notification)(nil), // 3: daemon.Notification
(*HelperResponse)(nil), // 4: daemon.HelperResponse
(*ConnectionOwner)(nil), // 5: daemon.ConnectionOwner
(*WIFIState)(nil), // 6: daemon.WIFIState
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
}
)
var file_daemon_helper_proto_depIdxs = []int32{
7, // 0: daemon.HelperRequest.getWIFIState:type_name -> google.protobuf.Empty
2, // 1: daemon.HelperRequest.findConnectionOwner:type_name -> daemon.FindConnectionOwnerRequest
3, // 2: daemon.HelperRequest.sendNotification:type_name -> daemon.Notification
6, // 3: daemon.HelperResponse.wifiState:type_name -> daemon.WIFIState
5, // 4: daemon.HelperResponse.connectionOwner:type_name -> daemon.ConnectionOwner
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_daemon_helper_proto_init() }
func file_daemon_helper_proto_init() {
if File_daemon_helper_proto != nil {
return
}
file_daemon_helper_proto_msgTypes[1].OneofWrappers = []any{
(*HelperRequest_GetWIFIState)(nil),
(*HelperRequest_FindConnectionOwner)(nil),
(*HelperRequest_SendNotification)(nil),
}
file_daemon_helper_proto_msgTypes[4].OneofWrappers = []any{
(*HelperResponse_WifiState)(nil),
(*HelperResponse_Error)(nil),
(*HelperResponse_ConnectionOwner)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_daemon_helper_proto_goTypes,
DependencyIndexes: file_daemon_helper_proto_depIdxs,
MessageInfos: file_daemon_helper_proto_msgTypes,
}.Build()
File_daemon_helper_proto = out.File
file_daemon_helper_proto_goTypes = nil
file_daemon_helper_proto_depIdxs = nil
}

View File

@@ -1,61 +0,0 @@
syntax = "proto3";
package daemon;
option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto";
message SubscribeHelperRequestRequest {
bool acceptGetWIFIStateRequests = 1;
bool acceptFindConnectionOwnerRequests = 2;
bool acceptSendNotificationRequests = 3;
}
message HelperRequest {
int64 id = 1;
oneof request {
google.protobuf.Empty getWIFIState = 2;
FindConnectionOwnerRequest findConnectionOwner = 3;
Notification sendNotification = 4;
}
}
message FindConnectionOwnerRequest {
int32 ipProtocol = 1;
string sourceAddress = 2;
int32 sourcePort = 3;
string destinationAddress = 4;
int32 destinationPort = 5;
}
message Notification {
string identifier = 1;
string typeName = 2;
int32 typeId = 3;
string title = 4;
string subtitle = 5;
string body = 6;
string openURL = 7;
}
message HelperResponse {
int64 id = 1;
oneof response {
WIFIState wifiState = 2;
string error = 3;
ConnectionOwner connectionOwner = 4;
}
}
message ConnectionOwner {
int32 userId = 1;
string userName = 2;
string processPath = 3;
string androidPackageName = 4;
}
message WIFIState {
string ssid = 1;
string bssid = 2;
}

View File

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

View File

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

View File

@@ -78,104 +78,55 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{0} return file_daemon_started_service_proto_rawDescGZIP(), []int{0}
} }
type ConnectionFilter int32 type ConnectionEventType int32
const ( const (
ConnectionFilter_ALL ConnectionFilter = 0 ConnectionEventType_CONNECTION_EVENT_NEW ConnectionEventType = 0
ConnectionFilter_ACTIVE ConnectionFilter = 1 ConnectionEventType_CONNECTION_EVENT_UPDATE ConnectionEventType = 1
ConnectionFilter_CLOSED ConnectionFilter = 2 ConnectionEventType_CONNECTION_EVENT_CLOSED ConnectionEventType = 2
) )
// Enum value maps for ConnectionFilter. // Enum value maps for ConnectionEventType.
var ( var (
ConnectionFilter_name = map[int32]string{ ConnectionEventType_name = map[int32]string{
0: "ALL", 0: "CONNECTION_EVENT_NEW",
1: "ACTIVE", 1: "CONNECTION_EVENT_UPDATE",
2: "CLOSED", 2: "CONNECTION_EVENT_CLOSED",
} }
ConnectionFilter_value = map[string]int32{ ConnectionEventType_value = map[string]int32{
"ALL": 0, "CONNECTION_EVENT_NEW": 0,
"ACTIVE": 1, "CONNECTION_EVENT_UPDATE": 1,
"CLOSED": 2, "CONNECTION_EVENT_CLOSED": 2,
} }
) )
func (x ConnectionFilter) Enum() *ConnectionFilter { func (x ConnectionEventType) Enum() *ConnectionEventType {
p := new(ConnectionFilter) p := new(ConnectionEventType)
*p = x *p = x
return p return p
} }
func (x ConnectionFilter) String() string { func (x ConnectionEventType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
} }
func (ConnectionFilter) Descriptor() protoreflect.EnumDescriptor { func (ConnectionEventType) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[1].Descriptor() return file_daemon_started_service_proto_enumTypes[1].Descriptor()
} }
func (ConnectionFilter) Type() protoreflect.EnumType { func (ConnectionEventType) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[1] return &file_daemon_started_service_proto_enumTypes[1]
} }
func (x ConnectionFilter) Number() protoreflect.EnumNumber { func (x ConnectionEventType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x) return protoreflect.EnumNumber(x)
} }
// Deprecated: Use ConnectionFilter.Descriptor instead. // Deprecated: Use ConnectionEventType.Descriptor instead.
func (ConnectionFilter) EnumDescriptor() ([]byte, []int) { func (ConnectionEventType) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{1} return file_daemon_started_service_proto_rawDescGZIP(), []int{1}
} }
type ConnectionSortBy int32
const (
ConnectionSortBy_DATE ConnectionSortBy = 0
ConnectionSortBy_TRAFFIC ConnectionSortBy = 1
ConnectionSortBy_TOTAL_TRAFFIC ConnectionSortBy = 2
)
// Enum value maps for ConnectionSortBy.
var (
ConnectionSortBy_name = map[int32]string{
0: "DATE",
1: "TRAFFIC",
2: "TOTAL_TRAFFIC",
}
ConnectionSortBy_value = map[string]int32{
"DATE": 0,
"TRAFFIC": 1,
"TOTAL_TRAFFIC": 2,
}
)
func (x ConnectionSortBy) Enum() *ConnectionSortBy {
p := new(ConnectionSortBy)
*p = x
return p
}
func (x ConnectionSortBy) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ConnectionSortBy) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[2].Descriptor()
}
func (ConnectionSortBy) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[2]
}
func (x ConnectionSortBy) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ConnectionSortBy.Descriptor instead.
func (ConnectionSortBy) EnumDescriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{2}
}
type ServiceStatus_Type int32 type ServiceStatus_Type int32
const ( const (
@@ -215,11 +166,11 @@ func (x ServiceStatus_Type) String() string {
} }
func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor { func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor {
return file_daemon_started_service_proto_enumTypes[3].Descriptor() return file_daemon_started_service_proto_enumTypes[2].Descriptor()
} }
func (ServiceStatus_Type) Type() protoreflect.EnumType { func (ServiceStatus_Type) Type() protoreflect.EnumType {
return &file_daemon_started_service_proto_enumTypes[3] return &file_daemon_started_service_proto_enumTypes[2]
} }
func (x ServiceStatus_Type) Number() protoreflect.EnumNumber { func (x ServiceStatus_Type) Number() protoreflect.EnumNumber {
@@ -1114,8 +1065,6 @@ func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
type SubscribeConnectionsRequest struct { type SubscribeConnectionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"` Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
Filter ConnectionFilter `protobuf:"varint,2,opt,name=filter,proto3,enum=daemon.ConnectionFilter" json:"filter,omitempty"`
SortBy ConnectionSortBy `protobuf:"varint,3,opt,name=sortBy,proto3,enum=daemon.ConnectionSortBy" json:"sortBy,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -1157,41 +1106,32 @@ func (x *SubscribeConnectionsRequest) GetInterval() int64 {
return 0 return 0
} }
func (x *SubscribeConnectionsRequest) GetFilter() ConnectionFilter { type ConnectionEvent struct {
if x != nil {
return x.Filter
}
return ConnectionFilter_ALL
}
func (x *SubscribeConnectionsRequest) GetSortBy() ConnectionSortBy {
if x != nil {
return x.SortBy
}
return ConnectionSortBy_DATE
}
type Connections struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Connections []*Connection `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"` Type ConnectionEventType `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.ConnectionEventType" json:"type,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
Connection *Connection `protobuf:"bytes,3,opt,name=connection,proto3" json:"connection,omitempty"`
UplinkDelta int64 `protobuf:"varint,4,opt,name=uplinkDelta,proto3" json:"uplinkDelta,omitempty"`
DownlinkDelta int64 `protobuf:"varint,5,opt,name=downlinkDelta,proto3" json:"downlinkDelta,omitempty"`
ClosedAt int64 `protobuf:"varint,6,opt,name=closedAt,proto3" json:"closedAt,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *Connections) Reset() { func (x *ConnectionEvent) Reset() {
*x = Connections{} *x = ConnectionEvent{}
mi := &file_daemon_started_service_proto_msgTypes[17] mi := &file_daemon_started_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
func (x *Connections) String() string { func (x *ConnectionEvent) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*Connections) ProtoMessage() {} func (*ConnectionEvent) ProtoMessage() {}
func (x *Connections) ProtoReflect() protoreflect.Message { func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[17] mi := &file_daemon_started_service_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1203,18 +1143,105 @@ func (x *Connections) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use Connections.ProtoReflect.Descriptor instead. // Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead.
func (*Connections) Descriptor() ([]byte, []int) { func (*ConnectionEvent) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{17} return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
} }
func (x *Connections) GetConnections() []*Connection { func (x *ConnectionEvent) GetType() ConnectionEventType {
if x != nil { if x != nil {
return x.Connections return x.Type
}
return ConnectionEventType_CONNECTION_EVENT_NEW
}
func (x *ConnectionEvent) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ConnectionEvent) GetConnection() *Connection {
if x != nil {
return x.Connection
} }
return nil return nil
} }
func (x *ConnectionEvent) GetUplinkDelta() int64 {
if x != nil {
return x.UplinkDelta
}
return 0
}
func (x *ConnectionEvent) GetDownlinkDelta() int64 {
if x != nil {
return x.DownlinkDelta
}
return 0
}
func (x *ConnectionEvent) GetClosedAt() int64 {
if x != nil {
return x.ClosedAt
}
return 0
}
type ConnectionEvents struct {
state protoimpl.MessageState `protogen:"open.v1"`
Events []*ConnectionEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnectionEvents) Reset() {
*x = ConnectionEvents{}
mi := &file_daemon_started_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnectionEvents) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnectionEvents) ProtoMessage() {}
func (x *ConnectionEvents) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead.
func (*ConnectionEvents) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
}
func (x *ConnectionEvents) GetEvents() []*ConnectionEvent {
if x != nil {
return x.Events
}
return nil
}
func (x *ConnectionEvents) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type Connection struct { type Connection 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"`
@@ -1238,13 +1265,14 @@ 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
} }
func (x *Connection) Reset() { func (x *Connection) Reset() {
*x = Connection{} *x = Connection{}
mi := &file_daemon_started_service_proto_msgTypes[18] mi := &file_daemon_started_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1256,7 +1284,7 @@ func (x *Connection) String() string {
func (*Connection) ProtoMessage() {} func (*Connection) ProtoMessage() {}
func (x *Connection) ProtoReflect() protoreflect.Message { func (x *Connection) ProtoReflect() protoreflect.Message {
mi := &file_daemon_started_service_proto_msgTypes[18] mi := &file_daemon_started_service_proto_msgTypes[19]
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 {
@@ -1269,7 +1297,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message {
// Deprecated: Use Connection.ProtoReflect.Descriptor instead. // Deprecated: Use Connection.ProtoReflect.Descriptor instead.
func (*Connection) Descriptor() ([]byte, []int) { func (*Connection) Descriptor() ([]byte, []int) {
return file_daemon_started_service_proto_rawDescGZIP(), []int{18} return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
} }
func (x *Connection) GetId() string { func (x *Connection) GetId() string {
@@ -1419,6 +1447,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[20]
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[20]
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{20}
}
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 +1539,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[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1440,7 +1551,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[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 {
@@ -1453,7 +1564,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{21}
} }
func (x *CloseConnectionRequest) GetId() string { func (x *CloseConnectionRequest) GetId() string {
@@ -1472,7 +1583,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[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1484,7 +1595,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[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 {
@@ -1497,7 +1608,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{22}
} }
func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
@@ -1518,7 +1629,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[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1530,7 +1641,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[23]
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 +1654,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{23}
} }
func (x *DeprecatedWarning) GetMessage() string { func (x *DeprecatedWarning) GetMessage() string {
@@ -1567,6 +1678,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[24]
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[24]
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{24}
}
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 +1732,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[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1589,7 +1744,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[25]
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 {
@@ -1623,7 +1778,7 @@ var File_daemon_started_service_proto protoreflect.FileDescriptor
const file_daemon_started_service_proto_rawDesc = "" + const file_daemon_started_service_proto_rawDesc = "" +
"\n" + "\n" +
"\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\x1a\x13daemon/helper.proto\"\xad\x01\n" + "\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xad\x01\n" +
"\rServiceStatus\x122\n" + "\rServiceStatus\x122\n" +
"\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" + "\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" +
"\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" +
@@ -1690,13 +1845,21 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" + "\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
"\x1cSetSystemProxyEnabledRequest\x12\x18\n" + "\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\"\x9d\x01\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\"9\n" +
"\x1bSubscribeConnectionsRequest\x12\x1a\n" + "\x1bSubscribeConnectionsRequest\x12\x1a\n" +
"\binterval\x18\x01 \x01(\x03R\binterval\x120\n" + "\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" +
"\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" + "\x0fConnectionEvent\x12/\n" +
"\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" + "\x04type\x18\x01 \x01(\x0e2\x1b.daemon.ConnectionEventTypeR\x04type\x12\x0e\n" +
"\vConnections\x124\n" + "\x02id\x18\x02 \x01(\tR\x02id\x122\n" +
"\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\xde\x04\n" + "\n" +
"connection\x18\x03 \x01(\v2\x12.daemon.ConnectionR\n" +
"connection\x12 \n" +
"\vuplinkDelta\x18\x04 \x01(\x03R\vuplinkDelta\x12$\n" +
"\rdownlinkDelta\x18\x05 \x01(\x03R\rdownlinkDelta\x12\x1a\n" +
"\bclosedAt\x18\x06 \x01(\x03R\bclosedAt\"Y\n" +
"\x10ConnectionEvents\x12/\n" +
"\x06events\x18\x01 \x03(\v2\x17.daemon.ConnectionEventR\x06events\x12\x14\n" +
"\x05reset\x18\x02 \x01(\bR\x05reset\"\x95\x05\n" +
"\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 +1883,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 +1898,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" +
@@ -1736,17 +1908,11 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x04WARN\x10\x03\x12\b\n" + "\x04WARN\x10\x03\x12\b\n" +
"\x04INFO\x10\x04\x12\t\n" + "\x04INFO\x10\x04\x12\t\n" +
"\x05DEBUG\x10\x05\x12\t\n" + "\x05DEBUG\x10\x05\x12\t\n" +
"\x05TRACE\x10\x06*3\n" + "\x05TRACE\x10\x06*i\n" +
"\x10ConnectionFilter\x12\a\n" + "\x13ConnectionEventType\x12\x18\n" +
"\x03ALL\x10\x00\x12\n" + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" +
"\n" + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" +
"\x06ACTIVE\x10\x01\x12\n" + "\x17CONNECTION_EVENT_CLOSED\x10\x022\xe5\v\n" +
"\n" +
"\x06CLOSED\x10\x02*<\n" +
"\x10ConnectionSortBy\x12\b\n" +
"\x04DATE\x10\x00\x12\v\n" +
"\aTRAFFIC\x10\x01\x12\x11\n" +
"\rTOTAL_TRAFFIC\x10\x022\xb7\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" +
@@ -1763,13 +1929,12 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + "\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
"\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" + "\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" +
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" + "\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n" + "\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" +
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x13.daemon.Connections\"\x000\x01\x12K\n" + "\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\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" +
"\x15SubscribeHelperEvents\x12\x16.google.protobuf.Empty\x1a\x15.daemon.HelperRequest\"\x000\x01\x12F\n" + "\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\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"
var ( var (
file_daemon_started_service_proto_rawDescOnce sync.Once file_daemon_started_service_proto_rawDescOnce sync.Once
@@ -1784,102 +1949,101 @@ 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, 3)
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 23) file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
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 (ConnectionEventType)(0), // 1: daemon.ConnectionEventType
(ConnectionSortBy)(0), // 2: daemon.ConnectionSortBy (ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type
(ServiceStatus_Type)(0), // 3: daemon.ServiceStatus.Type (*ServiceStatus)(nil), // 3: daemon.ServiceStatus
(*ServiceStatus)(nil), // 4: daemon.ServiceStatus (*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest
(*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest (*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest
(*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest (*Log)(nil), // 6: daemon.Log
(*Log)(nil), // 7: daemon.Log (*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel
(*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel (*Status)(nil), // 8: daemon.Status
(*Status)(nil), // 9: daemon.Status (*Groups)(nil), // 9: daemon.Groups
(*Groups)(nil), // 10: daemon.Groups (*Group)(nil), // 10: daemon.Group
(*Group)(nil), // 11: daemon.Group (*GroupItem)(nil), // 11: daemon.GroupItem
(*GroupItem)(nil), // 12: daemon.GroupItem (*URLTestRequest)(nil), // 12: daemon.URLTestRequest
(*URLTestRequest)(nil), // 13: daemon.URLTestRequest (*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest
(*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest (*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest
(*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest (*ClashMode)(nil), // 15: daemon.ClashMode
(*ClashMode)(nil), // 16: daemon.ClashMode (*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus
(*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus (*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus
(*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus (*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest
(*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest (*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest
(*SubscribeConnectionsRequest)(nil), // 20: daemon.SubscribeConnectionsRequest (*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent
(*Connections)(nil), // 21: daemon.Connections (*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents
(*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
} }
) )
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 2, // 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 10, // 3: daemon.Groups.group:type_name -> daemon.Group
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem 11, // 4: daemon.Group.items:type_name -> daemon.GroupItem
1, // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter 1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType
2, // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy 22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection
22, // 7: daemon.Connections.connections:type_name -> daemon.Connection 20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent
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
27, // 15: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty 29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
6, // 16: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest 29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
27, // 17: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty 5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
27, // 18: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty 29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
27, // 19: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty 29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
16, // 20: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode 29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
13, // 21: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest 15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
14, // 22: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest 12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
15, // 23: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest 13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
27, // 24: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty 14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
19, // 25: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest 29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
20, // 26: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest 18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
23, // 27: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest 19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
27, // 28: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty 24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
27, // 29: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty 29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
27, // 30: daemon.StartedService.SubscribeHelperEvents:input_type -> google.protobuf.Empty 29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
28, // 31: daemon.StartedService.SendHelperResponse:input_type -> daemon.HelperResponse 29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
27, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty 29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
27, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty 29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
4, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus 3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
7, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log 6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
8, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel 7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
27, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty 29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
9, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status 8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
10, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups 9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
17, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus 16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
16, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode 15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
27, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty 29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
27, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty 29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
27, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty 29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
27, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty 29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
18, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus 17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
27, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty 29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections 21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
27, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty 29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
27, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty 29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
24, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings 25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
29, // 52: daemon.StartedService.SubscribeHelperEvents:output_type -> daemon.HelperRequest 27, // 52: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
27, // 53: daemon.StartedService.SendHelperResponse:output_type -> google.protobuf.Empty 32, // [32:53] is the sub-list for method output_type
32, // [32:54] is the sub-list for method output_type 11, // [11:32] is the sub-list for method input_type
10, // [10:32] is the sub-list for method input_type 11, // [11:11] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension type_name 11, // [11:11] is the sub-list for extension extendee
10, // [10:10] is the sub-list for extension extendee 0, // [0:11] is the sub-list for field type_name
0, // [0:10] is the sub-list for field type_name
} }
func init() { file_daemon_started_service_proto_init() } func init() { file_daemon_started_service_proto_init() }
@@ -1887,14 +2051,13 @@ func file_daemon_started_service_proto_init() {
if File_daemon_started_service_proto != nil { if File_daemon_started_service_proto != nil {
return return
} }
file_daemon_helper_proto_init()
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
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: 3,
NumMessages: 23, NumMessages: 26,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -4,7 +4,6 @@ package daemon;
option go_package = "github.com/sagernet/sing-box/daemon"; option go_package = "github.com/sagernet/sing-box/daemon";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "daemon/helper.proto";
service StartedService { service StartedService {
rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty); rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty);
@@ -28,13 +27,11 @@ service StartedService {
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {} rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {} rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream Connections) {} rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
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 SendHelperResponse(HelperResponse) returns(google.protobuf.Empty) {}
} }
message ServiceStatus { message ServiceStatus {
@@ -146,24 +143,26 @@ message SetSystemProxyEnabledRequest {
message SubscribeConnectionsRequest { message SubscribeConnectionsRequest {
int64 interval = 1; int64 interval = 1;
ConnectionFilter filter = 2;
ConnectionSortBy sortBy = 3;
} }
enum ConnectionFilter { enum ConnectionEventType {
ALL = 0; CONNECTION_EVENT_NEW = 0;
ACTIVE = 1; CONNECTION_EVENT_UPDATE = 1;
CLOSED = 2; CONNECTION_EVENT_CLOSED = 2;
} }
enum ConnectionSortBy { message ConnectionEvent {
DATE = 0; ConnectionEventType type = 1;
TRAFFIC = 1; string id = 2;
TOTAL_TRAFFIC = 2; Connection connection = 3;
int64 uplinkDelta = 4;
int64 downlinkDelta = 5;
int64 closedAt = 6;
} }
message Connections { message ConnectionEvents {
repeated Connection connections = 1; repeated ConnectionEvent events = 1;
bool reset = 2;
} }
message Connection { message Connection {
@@ -188,6 +187,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 {
@@ -202,4 +210,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

@@ -35,8 +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_SubscribeHelperEvents_FullMethodName = "/daemon.StartedService/SubscribeHelperEvents" StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_SendHelperResponse_FullMethodName = "/daemon.StartedService/SendHelperResponse"
) )
// StartedServiceClient is the client API for StartedService service. // StartedServiceClient is the client API for StartedService service.
@@ -59,12 +58,11 @@ type StartedServiceClient interface {
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
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)
SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error)
} }
type startedServiceClient struct { type startedServiceClient struct {
@@ -280,13 +278,13 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
return out, nil return out, nil
} }
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error) { func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, Connections]{ClientStream: stream} x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil { if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err return nil, err
} }
@@ -297,7 +295,7 @@ func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *Sub
} }
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[Connections] type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[ConnectionEvents]
func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
@@ -329,29 +327,10 @@ func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *em
return out, nil return out, nil
} }
func (c *startedServiceClient) SubscribeHelperEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[HelperRequest], error) { func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, 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...) out := new(StartedAt)
if err != nil { err := c.cc.Invoke(ctx, StartedService_GetStartedAt_FullMethodName, in, out, cOpts...)
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, HelperRequest]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeHelperEventsClient = grpc.ServerStreamingClient[HelperRequest]
func (c *startedServiceClient) SendHelperResponse(ctx context.Context, in *HelperResponse, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_SendHelperResponse_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -378,12 +357,11 @@ type StartedServiceServer interface {
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
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)
SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error)
mustEmbedUnimplementedStartedServiceServer() mustEmbedUnimplementedStartedServiceServer()
} }
@@ -395,91 +373,87 @@ type StartedServiceServer interface {
type UnimplementedStartedServiceServer struct{} type UnimplementedStartedServiceServer struct{}
func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopService not implemented") return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
} }
func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReloadService not implemented") return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error { func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeServiceStatus not implemented") return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error { func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeLog not implemented") return status.Error(codes.Unimplemented, "method SubscribeLog not implemented")
} }
func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) { func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDefaultLogLevel not implemented") return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
} }
func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearLogs not implemented") return nil, status.Error(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.Error(codes.Unimplemented, "method SubscribeStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error { func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeGroups not implemented") return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented")
} }
func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) { func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetClashModeStatus not implemented") return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error { func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeClashMode not implemented") return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented")
} }
func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetClashMode not implemented") return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented")
} }
func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method URLTest not implemented") return nil, status.Error(codes.Unimplemented, "method URLTest not implemented")
} }
func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SelectOutbound not implemented") return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented")
} }
func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetGroupExpand not implemented") return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented")
} }
func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) { func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSystemProxyStatus not implemented") return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
} }
func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetSystemProxyEnabled not implemented") return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error { func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Errorf(codes.Unimplemented, "method SubscribeConnections not implemented") return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
} }
func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method CloseConnection not implemented") return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented")
} }
func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method CloseAllConnections not implemented") return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented")
} }
func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) { func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDeprecatedWarnings not implemented") return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
} }
func (UnimplementedStartedServiceServer) SubscribeHelperEvents(*emptypb.Empty, grpc.ServerStreamingServer[HelperRequest]) error { func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return status.Errorf(codes.Unimplemented, "method SubscribeHelperEvents not implemented") return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) SendHelperResponse(context.Context, *HelperResponse) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendHelperResponse not implemented")
} }
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -492,7 +466,7 @@ type UnsafeStartedServiceServer interface {
} }
func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) { func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) {
// If the following call pancis, it indicates UnimplementedStartedServiceServer was // If the following call panics, it indicates UnimplementedStartedServiceServer was
// embedded by pointer and is nil. This will cause panics if an // embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization // unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O. // time to prevent it from happening at runtime later due to I/O.
@@ -760,11 +734,11 @@ func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.S
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
return err return err
} }
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, Connections]{ServerStream: stream}) return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{ServerStream: stream})
} }
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[Connections] type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[ConnectionEvents]
func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CloseConnectionRequest) in := new(CloseConnectionRequest)
@@ -820,31 +794,20 @@ func _StartedService_GetDeprecatedWarnings_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StartedService_SubscribeHelperEvents_Handler(srv interface{}, stream grpc.ServerStream) error { func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
m := new(emptypb.Empty) in := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeHelperEvents(m, &grpc.GenericServerStream[emptypb.Empty, HelperRequest]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeHelperEventsServer = grpc.ServerStreamingServer[HelperRequest]
func _StartedService_SendHelperResponse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelperResponse)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(StartedServiceServer).SendHelperResponse(ctx, in) return srv.(StartedServiceServer).GetStartedAt(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: StartedService_SendHelperResponse_FullMethodName, FullMethod: StartedService_GetStartedAt_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).SendHelperResponse(ctx, req.(*HelperResponse)) return srv.(StartedServiceServer).GetStartedAt(ctx, req.(*emptypb.Empty))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@@ -913,8 +876,8 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_GetDeprecatedWarnings_Handler, Handler: _StartedService_GetDeprecatedWarnings_Handler,
}, },
{ {
MethodName: "SendHelperResponse", MethodName: "GetStartedAt",
Handler: _StartedService_SendHelperResponse_Handler, Handler: _StartedService_GetStartedAt_Handler,
}, },
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
@@ -948,11 +911,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeConnections_Handler, Handler: _StartedService_SubscribeConnections_Handler,
ServerStreams: true, ServerStreams: true,
}, },
{
StreamName: "SubscribeHelperEvents",
Handler: _StartedService_SubscribeHelperEvents_Handler,
ServerStreams: true,
},
}, },
Metadata: "daemon/started_service.proto", Metadata: "daemon/started_service.proto",
} }

View File

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

View File

@@ -144,7 +144,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if c.cache != nil { if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{})) cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded { if loaded {
<-cond select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else { } else {
defer func() { defer func() {
c.cacheLock.Delete(question) c.cacheLock.Delete(question)
@@ -154,7 +158,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} else if c.transportCache != nil { } else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{})) cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded { if loaded {
<-cond select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else { } else {
defer func() { defer func() {
c.transportCacheLock.Delete(question) c.transportCacheLock.Delete(question)
@@ -232,8 +240,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
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
if response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0 { if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true rejected = true
} else if len(response.Answer) == 0 {
rejected = !responseChecker(nil)
} else { } else {
rejected = !responseChecker(MessageToAddresses(response)) rejected = !responseChecker(MessageToAddresses(response))
} }
@@ -314,16 +324,20 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
} else { } else {
strategy = options.Strategy strategy = options.Strategy
} }
lookupOptions := options
if options.LookupStrategy != C.DomainStrategyAsIS {
lookupOptions.Strategy = strategy
}
if strategy == C.DomainStrategyIPv4Only { if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
} else if strategy == C.DomainStrategyIPv6Only { } else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
} }
var response4 []netip.Addr var response4 []netip.Addr
var response6 []netip.Addr var response6 []netip.Addr
var group task.Group var group task.Group
group.Append("exchange4", func(ctx context.Context) error { group.Append("exchange4", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
if err != nil { if err != nil {
return err return err
} }
@@ -331,7 +345,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
return nil return nil
}) })
group.Append("exchange6", func(ctx context.Context) error { group.Append("exchange6", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
if err != nil { if err != nil {
return err return err
} }
@@ -353,68 +367,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)
if response4 == nil {
return nil, false
}
response6, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if response6 == nil {
return nil, false
}
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

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

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

@@ -0,0 +1,287 @@
package transport
import (
"context"
"net"
"sync"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
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
connectionCancel context.CancelFunc
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)
},
})
}
type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
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.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if isRecursiveConnectorDial(ctx, c) {
c.access.Unlock()
return zero, errRecursiveConnectorDial
}
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
}
}
if err := ctx.Err(); err != nil {
c.access.Unlock()
return zero, err
}
c.connecting = make(chan struct{})
c.access.Unlock()
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
connection, cancel, err := c.dialWithCancellation(dialContext)
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, ErrTransportClosed
}
if err = ctx.Err(); err != nil {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, err
}
c.connection = connection
c.hasConnection = true
c.connectionCancel = cancel
result := c.connection
c.access.Unlock()
return result, nil
}
}
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
return loaded && dialConnector == connector
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, nil, err
}
connCtx, cancel := context.WithCancel(c.closeCtx)
var (
stateAccess sync.Mutex
dialComplete bool
)
stopCancel := context.AfterFunc(ctx, func() {
stateAccess.Lock()
if !dialComplete {
cancel()
}
stateAccess.Unlock()
})
select {
case <-ctx.Done():
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
cancel()
return zero, nil, ctx.Err()
default:
}
connection, err := c.dial(valueContext{connCtx, ctx})
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
if err != nil {
cancel()
return zero, nil, err
}
return connection, cancel, nil
}
type valueContext struct {
context.Context
parent context.Context
}
func (v valueContext) Value(key any) any {
return v.parent.Value(key)
}
func (v valueContext) Deadline() (time.Time, bool) {
return v.parent.Deadline()
}
func (c *Connector[T]) Close() error {
c.access.Lock()
defer c.access.Unlock()
if c.closed {
return nil
}
c.closed = true
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
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.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
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

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

View File

@@ -108,6 +108,13 @@ 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 {
@@ -256,6 +263,7 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
defer buffer.Release() defer buffer.Release()
for { for {
buffer.Reset()
_, _, err := buffer.ReadPacketFrom(packetConn) _, _, err := buffer.ReadPacketFrom(packetConn)
if err != nil { if err != nil {
if errors.Is(err, io.ErrShortBuffer) { if errors.Is(err, io.ErrShortBuffer) {

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,22 +14,49 @@ 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 inet4Last netip.Addr
inet4Current netip.Addr inet6Last netip.Addr
inet6Current netip.Addr storage adapter.FakeIPStorage
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 {
return &Store{ store := &Store{
ctx: ctx, ctx: ctx,
logger: logger, logger: logger,
inet4Range: inet4Range, inet4Range: inet4Range,
inet6Range: inet6Range, inet6Range: inet6Range,
} }
if inet4Range.IsValid() {
store.inet4Last = broadcastAddress(inet4Range)
}
if inet6Range.IsValid() {
store.inet6Last = broadcastAddress(inet6Range)
}
return store
}
func broadcastAddress(prefix netip.Prefix) netip.Addr {
addr := prefix.Addr()
raw := addr.As16()
bits := prefix.Bits()
if addr.Is4() {
bits += 96
}
for i := bits; i < 128; i++ {
raw[i/8] |= 1 << (7 - i%8)
}
if addr.Is4() {
return netip.AddrFrom4([4]byte(raw[12:]))
}
return netip.AddrFrom16(raw)
} }
func (s *Store) Start() error { func (s *Store) Start() error {
@@ -46,10 +74,10 @@ func (s *Store) Start() error {
s.inet6Current = metadata.Inet6Current s.inet6Current = metadata.Inet6Current
} else { } else {
if s.inet4Range.IsValid() { if s.inet4Range.IsValid() {
s.inet4Current = s.inet4Range.Addr().Next().Next() s.inet4Current = s.inet4Range.Addr().Next()
} }
if s.inet6Range.IsValid() { if s.inet6Range.IsValid() {
s.inet6Current = s.inet6Range.Addr().Next().Next() s.inet6Current = s.inet6Range.Addr().Next()
} }
_ = storage.FakeIPReset() _ = storage.FakeIPReset()
} }
@@ -65,25 +93,37 @@ 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() {
return netip.Addr{}, E.New("missing IPv4 fakeip address range") return netip.Addr{}, E.New("missing IPv4 fakeip address range")
} }
nextAddress := s.inet4Current.Next() nextAddress := s.inet4Current.Next()
if !s.inet4Range.Contains(nextAddress) { if nextAddress == s.inet4Last || !s.inet4Range.Contains(nextAddress) {
nextAddress = s.inet4Range.Addr().Next().Next() nextAddress = s.inet4Range.Addr().Next().Next()
} }
s.inet4Current = nextAddress s.inet4Current = nextAddress
@@ -93,13 +133,16 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
return netip.Addr{}, E.New("missing IPv6 fakeip address range") return netip.Addr{}, E.New("missing IPv6 fakeip address range")
} }
nextAddress := s.inet6Current.Next() nextAddress := s.inet6Current.Next()
if !s.inet6Range.Contains(nextAddress) { if nextAddress == s.inet6Last || !s.inet6Range.Contains(nextAddress) {
nextAddress = s.inet6Range.Addr().Next().Next() nextAddress = s.inet6Range.Addr().Next().Next()
} }
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

@@ -76,12 +76,12 @@ 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() return t.resolved.Exchange(ctx, message)
if resolverObject != nil {
return t.resolved.Exchange(resolverObject, ctx, message)
}
} }
question := message.Question[0] question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

View File

@@ -92,6 +92,12 @@ 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]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

View File

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

View File

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

View File

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

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{}
}

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