Compare commits

..

96 Commits

Author SHA1 Message Date
Bruce Wayne
db8b42c107 fix(ocm): map prolite to lower pro weight 2026-04-12 01:44:42 +08:00
世界
bf9e390cf4 fix(ccm,ocm): allow reverse connector credentials to serve local requests
Connector mode credentials were unconditionally blocked from local use
by unavailableError(), despite having a working forwardHTTPClient. Also
set credentialDialer in OCM connector mode to prevent nil panic in
WebSocket handler.
2026-03-28 18:46:10 +08:00
世界
471c9c3b47 fix(ccm): make refresh failure fail fast 2026-03-28 18:07:31 +08:00
世界
e7478ce947 fix(ccm): mark credential unavailable on refresh failure, handle poll 401
tryRefreshCredentials now returns error and calls markCredentialsUnavailable
when lock acquisition or file write permission fails. getAccessToken propagates
the error instead of silently returning the expired token. pollUsage handles
401 by attempting auth recovery and marking unavailable on failure. All
credential error paths now use Error log level instead of Debug. Startup
checks expired tokens eagerly via tryRefreshCredentials.
2026-03-28 16:58:52 +08:00
世界
cf11e0e74a Reuse SDK JSON types in ccm and ocm 2026-03-28 11:20:24 +08:00
世界
a87a2b0e2b ocm: log think level 2026-03-28 02:00:27 +08:00
世界
d9c298af1e fix(ccm,ocm): remove upstream rate limit header forwarding, compute locally
Strip all upstream rate limit headers and compute unified-status,
representative-claim, reset times, and surpassed-threshold from
aggregated utilization data. Never expose per-account overage or
fallback information. Remove per-credential unified state storage,
snapshot aggregation, and WebSocket synthetic rate limit events.
2026-03-26 23:42:14 +08:00
世界
cd5007ffbb fix(ccm,ocm): track external credential poll failures and re-poll on user connect
External credentials now properly increment consecutivePollFailures on
poll errors (matching defaultCredential behavior), marking the credential
as temporarily blocked. When a user with external_credential connects
and the credential is not usable, a forced poll is triggered to check
recovery.
2026-03-26 22:16:02 +08:00
世界
e49d0685ad ccm: Fix token refresh 2026-03-26 21:37:12 +08:00
世界
e1c9667319 Revert "fix(ocm): send rate limit status immediately on WebSocket connect"
This reverts commit 6721dff48a.
2026-03-26 16:03:46 +08:00
世界
6f45ea9c27 Revert "fix(ocm): inject synthetic rate limits inline when intercepting upstream events"
This reverts commit ca60f93184.
2026-03-26 16:03:39 +08:00
世界
ca60f93184 fix(ocm): inject synthetic rate limits inline when intercepting upstream events
The initial synthetic event from 6721dff48 arrives before the Codex CLI's
response stream reader is active. Additionally, the shouldEmit gate in
updateStateFromHeaders suppresses the async replacement when values haven't
changed. Send aggregated status inline in proxyWebSocketUpstreamToClient
so the client receives it at the exact protocol position it expects.
2026-03-26 15:42:51 +08:00
世界
1774d98793 fix(ccm,ocm): restore fixed usage polling
Remove the poll_interval config surface from CCM and OCM so both services fall back to the built-in 1h polling cadence again. Also isolate CCM credential lock mocking per test instance so the access-token refresh tests stop racing on shared global state.
2026-03-26 14:01:24 +08:00
世界
6721dff48a fix(ocm): send rate limit status immediately on WebSocket connect
Codex CLI ignores x-codex-* headers in the WebSocket upgrade response
and only reads rate limits from in-band codex.rate_limits events.
Previously, the first synthetic event was gated by firstRealRequest
(after warmup), delaying usage display. Now send aggregated status
right after subscribing, so the client sees rate limits before the
first turn begins.
2026-03-26 12:33:53 +08:00
世界
4592164a7a Align CCM and OCM rate limits 2026-03-24 22:06:10 +08:00
世界
92c8f4c5c8 fix(ccm): align default credential with Claude Code 2026-03-24 21:15:46 +08:00
世界
441c98890d feat(ccm): add claude_directory option to read Claude Code config 2026-03-22 16:59:36 +08:00
世界
bb2169bc17 release: Fix install_go.sh 2026-03-22 06:35:34 +08:00
世界
d996b60f44 ccm/ocm: Add CLAUDE.md 2026-03-22 06:23:13 +08:00
世界
084a6f1302 fix(ccm): align OAuth token refresh with Claude Code v2.1.81
After re-login with newer Claude Code (v2.1.75+), CCM refresh requests
returned persistent 429s. Root cause: CCM omitted the `scope` parameter
that the server now requires for tokens with `user:file_upload` scope.

Changes to fully match Claude Code's OAuth behavior:

- Add `scope` parameter to token refresh request body
- Parse `scope` from refresh response and store back
- Add `subscriptionType`/`rateLimitTier` to credential struct to
  preserve Claude Code's profile state on write-back
- Change credential file write to read-modify-write, preserving
  other top-level JSON keys (matches Claude Code's BP6 pattern)
- Same for macOS keychain write path
- Increase token expiry buffer from 1 min to 5 min (matching CC's
  isOAuthTokenExpired with 300s buffer)
- Add cross-process mkdir-based file lock compatible with Claude
  Code's proper-lockfile protocol (~/.claude.lock)
- Add post-failure recovery: re-read credentials from disk after
  refresh failure in case another process succeeded
- Add 401/403 "OAuth token has been revoked" recovery in proxy
  handler: reload credentials and retry once
2026-03-22 06:02:55 +08:00
世界
0950783479 fix(ccm,ocm): exclude unusable credentials from status aggregation
computeAggregatedUtilization used isAvailable() which only checks
permanent unavailability, so credentials rejected by upstream 400
still had their planWeight included in the total, inflating reported
capacity and diluting utilization.
2026-03-21 11:42:49 +08:00
世界
f172a575b7 fix(ccm): log assigned credential for each distinct model per session 2026-03-21 11:07:43 +08:00
世界
29b901a8b3 fix(ccm): robust account UUID injection and session ID validation
Replace bytes.Replace-based UUID injection with proper JSON
unmarshal/re-marshal through map[string]json.RawMessage — the old
approach silently failed when the body used non-canonical JSON escaping.

Return 500 when metadata.user_id is present but in an unrecognized
format, instead of silently passing through with an empty session ID.
2026-03-21 11:00:05 +08:00
世界
53f832330d fix(ccm): adapt to Claude Code v2.1.78 metadata format, separate state from credentials
Claude Code v2.1.78 changed metadata.user_id from a template literal
(`user_${id}_account_${uuid}_session_${sid}`) to a JSON-encoded object
(`JSON.stringify({device_id, account_uuid, session_id})`), breaking
session ID extraction via `_session_` substring match.

- Fix extractCCMSessionID to try JSON parse first, fallback to legacy
- Remove subscriptionType/rateLimitTier/isMax from oauthCredentials
  (profile state does not belong in auth credentials)
- Add state_path option for persisting profile state across restarts
- Parse account.uuid from /api/oauth/profile response
- Inject account_uuid into forwarded requests when client sends it empty
  (happens when using ANTHROPIC_AUTH_TOKEN instead of Claude AI OAuth)
2026-03-21 10:45:24 +08:00
世界
99d9e06dd0 fix(ccm,ocm): handle upstream 400 by marking external credentials rejected and polling default credentials
External credentials returning 400 are marked unavailable for pollInterval
duration; status stream/poll success clears the rejection early. Default
credentials trigger a stale poll to let the usage API detect account issues
without causing 429 storms.
2026-03-21 10:31:17 +08:00
世界
608b7e7fa2 fix(ccm,ocm): stop cascading 429 retry storm on token refresh
When the access token expires and refreshToken() gets 429, getAccessToken()
returned the error but left credentials unchanged with no cooldown. Every
subsequent request re-attempted the refresh, creating a burst that overwhelmed
the token endpoint.

- refreshToken() now returns Retry-After duration from 429 response headers
  (-1 when no header present, meaning permanently blocked)
- getAccessToken() caches the 429 and blocks further refresh attempts until
  Retry-After expires (or permanently if no header)
- reloadCredentials() clears the block when new credentials are loaded from file
- Remove go pollUsage() on upstream errors (unrelated to usage state)
2026-03-21 09:31:05 +08:00
世界
7acba74755 fix(ccm): forward 529 upstream overloaded response transparently 2026-03-18 15:53:36 +08:00
世界
2fe1e37b17 fix(ccm,ocm): add missing isFirstUpdate to external credential usage logging 2026-03-18 01:00:55 +08:00
世界
3bcfdd5455 fix(ccm,ocm): remove external context from pollUsage/pollIfStale
pollUsage(ctx) accepted caller context, and service_status.go passed
r.Context() which gets canceled on client disconnect or service shutdown.
This caused incrementPollFailures → interruptConnections on transient
cancellations. Each implementation now uses its own persistent context:
defaultCredential uses serviceContext, externalCredential uses
getReverseContext().
2026-03-18 00:54:01 +08:00
世界
b119d08764 fix(ccm,ocm): add usage logging to status stream, remove redundant isFirstUpdate
connectStatusStream updated credential state silently — no log on
first frame or value changes. After restart, external credentials
get usage via stream before any request, so pollIfStale skips them
and no usage log ever appears.

Add the same change-detection log to connectStatusStream. Also remove
redundant isFirstUpdate guards from pollUsage and updateStateFromHeaders:
when old values are zero, any non-zero new value already satisfies the
integer-percent comparison.
2026-03-17 22:37:38 +08:00
世界
6b8838d323 fix(ccm,ocm): restart status stream when receiver gets reverse session
statusStreamLoop started on start() before any reverse session existed,
got a non-retryable error, and exited permanently. Restart it when
setReverseSession transitions receiver credentials to available.
2026-03-17 22:08:30 +08:00
世界
b3429ef1f3 fix(ocm): strip non-active rate-limit headers from forwarded responses 2026-03-17 22:01:30 +08:00
世界
a2d6cf9715 fix(ocm): defer initial websocket rate-limit push 2026-03-17 21:14:14 +08:00
世界
99e19e7033 service: stop retrying fatal watch status errors 2026-03-17 20:47:52 +08:00
世界
969defeef0 ccm,ocm: validate external status response fields 2026-03-17 20:17:56 +08:00
世界
f57eff33bb ccm,ocm: fix WS push lifecycle, deduplicate rate_limits, stabilize reset aggregation
- Add closed channel to webSocketSession for push goroutine shutdown
  on connection close, preventing session leak and Service.Close() hang
- Intercept upstream codex.rate_limits events instead of forwarding;
  push goroutine is now the sole sender of aggregated rate_limits
- Emit status updates on reset-only changes (fiveHourResetChanged,
  weeklyResetChanged) so push goroutine picks up reset advances
- Skip expired resets (hours <= 0) in aggregation instead of clamping
  to now, avoiding unstable reset_at output and spurious status ticks
- Delete stale upstream reset headers when aggregated reset is zero
- Hardcode "codex" identifier everywhere: handleWebSocketRateLimitsEvent,
  buildSyntheticRateLimitsEvent, rewriteResponseHeaders
- Remove rewriteWebSocketRateLimits, rewriteWebSocketRateLimitWindow,
  identifier tracking (TypedValue), and unused imports
2026-03-17 20:00:54 +08:00
世界
0a054b9aa4 ccm,ocm: propagate reset times, rewrite headers for all users, add WS status push
- Add fiveHourReset/weeklyReset to statusPayload and aggregatedStatus
  with weight-averaged reset time aggregation across credential pools
- Rewrite response headers (utilization + reset times) for all users,
  not just external credential users
- Rewrite WebSocket rate_limits events for all users with aggregated values
- Add proactive WebSocket status push: synthetic codex.rate_limits events
  sent on connection start and on status changes via statusObserver
- Remove one-shot stream forward compatibility (statusStreamHeader,
  restoreLastUpdatedIfUnchanged, oneShot detection)
2026-03-17 18:13:54 +08:00
世界
7d15d9d282 ccm: emit status updates for plan-weight-only changes 2026-03-17 16:46:54 +08:00
世界
cf2d677043 ocm: emit status updates for plan-weight-only changes 2026-03-17 16:32:03 +08:00
世界
4a6a211775 ccm,ocm: reduce status emission noise, simplify emit-guard pattern
Guard updateStateFromHeaders emission with value-change detection to
avoid unnecessary computeAggregatedUtilization scans on every proxied
response. Replace statusAggregateStateLocked two-value return with
comparable statusSnapshot struct. Define statusPayload type for the
status wire format, replacing anonymous structs and map literals.
2026-03-17 16:10:59 +08:00
世界
f84832a369 Add stream watch endpoint 2026-03-17 16:03:35 +08:00
世界
f3c3022094 ccm,ocm: fix session race, track fallback sessions, skip warmup logging
Fix data race in selectCredential where concurrent goroutines could
overwrite each other's session entries by adding compare-and-delete
and store-if-absent patterns with retry loop. Track sessions for
fallback strategy so isNew is reported correctly. Skip logging and
usage tracking for websocket warmup requests (generate: false).
2026-03-16 22:10:10 +08:00
世界
2dd093a32e ccm,ocm: fix data race, remove dead code, clean up inefficiencies 2026-03-15 21:20:29 +08:00
世界
14ade76956 ccm,ocm: remove dead code, fix timer leaks, eliminate redundant lookups
- Remove unused onBecameUnusable field from CCM credential structs
  (OCM wires it for WebSocket interruption; CCM has no equivalent)
- Replace time.After with time.NewTimer in doHTTPWithRetry and
  connectorLoop to avoid timer leaks on context cancellation
- Pass already-resolved provider to rewriteResponseHeadersForExternalUser
  instead of re-resolving via credentialForUser
- Hoist reverseYamuxConfig to package-level var (immutable, no need to
  allocate on every call)
2026-03-15 20:42:41 +08:00
世界
9e3ec30d72 docs: fix ccm and ocm credential docs 2026-03-15 20:41:47 +08:00
世界
763e0af010 docs: complete ccm/ocm documentation for 1.14.0 features 2026-03-15 18:49:00 +08:00
世界
656b09d1be ccm,ocm: never treat external usage endpoint failures as over-limit 2026-03-15 18:48:53 +08:00
世界
8e9c61e624 ccm,ocm: normalize legacy fields into credentials at init, remove dual code path 2026-03-15 18:48:53 +08:00
世界
bc6e72408d ccm,ocm: block API key headers from being forwarded upstream 2026-03-15 18:48:52 +08:00
世界
56af7313b2 ccm,ocm: don't treat usage API 429 as account over-limit
The usage API itself has rate limits. A 429 from it means "poll less
frequently", not that the account exceeded its usage quota. Previously
incrementPollFailures() was called, marking the credential unusable and
interrupting in-flight connections.

Now: parse Retry-After, store as usageAPIRetryDelay, and retry after
that delay. The credential stays usable and relies on passive header
updates for usage data in the meantime.
2026-03-15 18:48:52 +08:00
世界
6878ad0d35 ccm,ocm: fix naming and error-handling convention violations
- Rename credential interface to Credential (exported), cred to credential
- Rename mutex/saveMutex to access/saveAccess per go-syntax.md
- Fix abbreviations: reverseHttpClient, allCreds, credOpt, extCred,
  credDialer, reverseCredDialer, portStr
- Replace errors.Is(http.ErrServerClosed) with E.IsClosed
- Add E.IsClosedOrCanceled guard before streaming write error logs
2026-03-15 18:48:51 +08:00
世界
04bd63b455 ccm,ocm: reorganize files and improve naming conventions
Split credential_state.go (1500+ lines) into credential.go,
credential_default.go, credential_provider.go, credential_builder.go.

Split service.go (900+ lines) into service.go, service_handler.go,
service_status.go.

Rename credential.go to credential_oauth.go to avoid name conflict
with the credential interface.

Apply naming fixes: accessMutex→access, stateMutex→stateAccess,
sessionMutex→sessionAccess, webSocketMutex→webSocketAccess,
httpTransport()→httpClient(), httpClient field→forwardHTTPClient,
weeklyWindowDuration→weeklyWindowHours.
2026-03-15 18:48:51 +08:00
世界
51d564c9ff ccm,ocm: merge fallback into balancer strategy, use hyphenated constant names
Merge the fallback credential type into balancer as a strategy
(C.BalancerStrategyFallback). Replace raw string literals with
C.BalancerStrategyXxx constants and switch to hyphens (least-used,
round-robin) per project convention.
2026-03-15 18:48:50 +08:00
世界
4d8baf7175 ccm: fix nil pointer in pollUsage for connector-mode credentials
Connector-mode credentials (URL + reverse: true) never assigned
httpClient, causing a nil dereference when pollUsage accessed
httpClient.Transport.

Also extract poll request logic into doPollUsageRequest to try
reverse transport first (single attempt), then fall back to
forward transport with retries if the reverse session disconnects.
2026-03-15 18:48:50 +08:00
世界
d1e5426bc8 ccm,ocm: add exponential backoff with cap for poll retry
Replace flat 1-minute poll retry interval with exponential backoff
(1m → 2m → 4m → 5m cap). Suppress error logs after reaching the cap.
2026-03-15 18:48:50 +08:00
世界
4d907bc49d ccm,ocm: allow URL-based credentials to accept reverse connections
Previously, findReceiverCredential required baseURL == reverseProxyBaseURL,
so only credentials with no URL could accept incoming reverse connections.
Now credentials with a normal URL also accept reverse connections, preferring
the reverse session when active and falling back to the direct URL when not.
2026-03-15 18:48:49 +08:00
世界
2c907bef2c Fix scoped rebalance interrupts 2026-03-15 18:48:49 +08:00
世界
d2300353fd Propagate request context to upstream requests 2026-03-15 18:48:49 +08:00
世界
f871113832 ccm,ocm: add balancer session rebalancing with per-credential interrupt
When a sticky session's credential utilization exceeds the least-used
credential by a weight-adjusted threshold, force reassign all sessions
on that credential and cancel in-flight requests scoped to the balancer.

Threshold formula: effective = rebalance_threshold / planWeight, so a
config value of 20 triggers at 2% delta for Max 20x (w=10), 4% for
Max 5x (w=5), and 20% for Pro (w=1).
2026-03-15 18:48:49 +08:00
世界
b97b9d9cfd ccm,ocm: add request ID context to HTTP request logging 2026-03-15 18:48:48 +08:00
世界
badeeb91fe 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-15 18:48:48 +08:00
世界
f4aaf33bf2 ccm,ocm: strip reverse proxy headers from upstream responses 2026-03-15 18:48:48 +08:00
世界
8fe8e238b3 service/ocm: unify websocket logging with HTTP request logging 2026-03-15 18:48:47 +08:00
世界
6f433937ba ccm,ocm: auto-detect plan weight for external credentials via status endpoint 2026-03-15 18:48:47 +08:00
世界
80d5432654 service/ccm: update oauth token URL and remove unnecessary Accept header 2026-03-15 18:48:46 +08:00
世界
8984b45ded 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-15 18:48:46 +08:00
世界
25a9e4ce59 service/ocm: only log new credential assignments and add websocket logging 2026-03-15 18:48:46 +08:00
世界
615a7e05b4 service/ccm: only log new credential assignments and show context window in model 2026-03-15 18:48:46 +08:00
世界
1628272507 ccm,ocm: mark credentials unusable on usage poll failure and trigger poll on upstream error 2026-03-15 18:48:46 +08:00
世界
ee65b375cb 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-15 18:48:45 +08:00
世界
a09174a9a2 service/ccm: reject fast-mode external credentials 2026-03-15 18:48:45 +08:00
世界
ce543a935f ccm,ocm: fix reserveWeekly default and remove dead reserve fields 2026-03-15 18:48:45 +08:00
世界
7f93c76b1a 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-15 18:48:44 +08:00
世界
df6e47f5f1 ocm: preserve websocket rate limit event fields 2026-03-15 18:48:44 +08:00
世界
1993da3735 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-15 18:48:43 +08:00
世界
22376472d0 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-15 18:48:43 +08:00
世界
74bf20d349 ccm,ocm: fix reverse session shutdown race 2026-03-15 18:48:43 +08:00
世界
ff8585f7c6 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-15 18:48:42 +08:00
世界
4d5108fe7f 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-15 18:48:42 +08:00
世界
3b177df05e 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-15 18:48:42 +08:00
世界
1824881719 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-15 18:48:41 +08:00
世界
02a1409e9a ccm,ocm: unify HTTP request retry with fast retry and exponential backoff 2026-03-15 18:48:41 +08:00
世界
af94ea9089 Fix reverse external credential handling 2026-03-15 18:48:41 +08:00
世界
970951f369 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-15 18:48:40 +08:00
世界
15f3619995 ccm,ocm: strip reverse proxy headers before forwarding to upstream 2026-03-15 18:48:40 +08:00
世界
b96ab4fef9 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-15 18:48:40 +08:00
世界
6829f91a06 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-15 18:48:40 +08:00
世界
8e5811a8c7 ccm,ocm: watch credential_path and allow delayed credentials 2026-03-15 18:48:40 +08:00
世界
da8ff6f578 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-15 18:48:39 +08:00
世界
2801bce815 ccm/ocm: Add multi-credential support with balancer and fallback strategies 2026-03-15 18:48:39 +08:00
世界
a11cd1e0c6 Bump version 2026-03-15 17:57:54 +08:00
世界
bd0fb83d2d cronet-go: Update chromium to 145.0.7632.159 2026-03-15 17:57:54 +08:00
世界
9462b1deeb documentation: Update descriptions for neighbor rules 2026-03-15 17:57:53 +08:00
世界
44d1c86b1b Add macOS support for MAC and hostname rule items 2026-03-15 17:57:53 +08:00
世界
f802668915 Add Android support for MAC and hostname rule items 2026-03-15 17:57:53 +08:00
世界
4d217b7481 Add MAC and hostname rule items 2026-03-15 17:57:53 +08:00
211 changed files with 16397 additions and 3888 deletions

View File

@@ -4,7 +4,6 @@
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--vendor SagerNet
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes

View File

@@ -1 +1 @@
2fef65f9dba90ddb89a87d00a6eb6165487c10c1
ea7cd33752aed62603775af3df946c1b83f4b0b3

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
branches=$(git branch -r --contains HEAD)
if echo "$branches" | grep -q 'origin/stable'; then
track=stable
elif echo "$branches" | grep -q 'origin/testing'; then
track=testing
elif echo "$branches" | grep -q 'origin/oldstable'; then
track=oldstable
else
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
exit 1
fi
if [[ "$track" == "stable" ]]; then
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
track=beta
fi
fi
case "$track" in
stable) name=sing-box; docker_tag=latest ;;
beta) name=sing-box-beta; docker_tag=latest-beta ;;
testing) name=sing-box-testing; docker_tag=latest-testing ;;
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
esac
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
echo "TRACK=${track}" >> "$GITHUB_ENV"
echo "NAME=${name}" >> "$GITHUB_ENV"
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"

View File

@@ -19,6 +19,7 @@ env:
jobs:
build_binary:
name: Build binary
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
strategy:
fail-fast: true
@@ -259,13 +260,13 @@ jobs:
fi
echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0
- name: Detect track
run: bash .github/detect_track.sh
if [[ $ref == *"-"* ]]; then
latest=latest-beta
else
latest=latest
fi
echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests
uses: actions/download-artifact@v5
with:
@@ -285,11 +286,11 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
if: github.event_name != 'push'
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -11,6 +11,11 @@ on:
description: "Version name"
required: true
type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release:
types:
- published
@@ -18,6 +23,7 @@ on:
jobs:
calculate_version:
name: Calculate version
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
@@ -162,8 +168,14 @@ jobs:
- name: Set mtime
run: |-
TZ=UTC touch -t '197001010000' dist/sing-box
- name: Detect track
run: bash .github/detect_track.sh
- name: Set name
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"

4
.gitignore vendored
View File

@@ -18,6 +18,6 @@
.DS_Store
/config.d/
/venv/
CLAUDE.md
AGENTS.md
/CLAUDE.md
/AGENTS.md
/.claude/

View File

@@ -2,6 +2,7 @@ package adapter
import (
"context"
"net"
"net/netip"
"time"
@@ -82,6 +83,8 @@ type InboundContext struct {
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
@@ -101,10 +104,6 @@ type InboundContext struct {
func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.ResetRuleMatchCache()
}
func (c *InboundContext) ResetRuleMatchCache() {
c.SourceAddressMatch = false
c.SourcePortMatch = false
c.DestinationAddressMatch = false

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

@@ -36,6 +36,10 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type FindConnectionOwnerRequest struct {
@@ -47,11 +51,11 @@ type FindConnectionOwnerRequest struct {
}
type ConnectionOwner struct {
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageNames []string
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageName string
}
type Notification struct {

View File

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

10
box.go
View File

@@ -19,6 +19,7 @@ import (
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/log"
@@ -325,12 +326,11 @@ func New(options Options) (*Box, error) {
)
})
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
return dnsTransportRegistry.CreateDNSTransport(
return local.NewTransport(
ctx,
logFactory.NewLogger("dns/local"),
"local",
C.DNSTypeLocal,
&option.LocalDNSServerOptions{},
option.LocalDNSServerOptions{},
)
})
if platformInterface != nil {
@@ -555,10 +555,6 @@ func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}
func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}

View File

@@ -149,10 +149,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else {
dialer.Timeout = C.TCPConnectTimeout
}
if options.DisableTCPKeepAlive {
dialer.KeepAlive = -1
dialer.KeepAliveConfig.Enable = false
} else {
if !options.DisableTCPKeepAlive {
keepIdle := time.Duration(options.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial
@@ -242,7 +239,7 @@ func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefaul
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() {
return nil, E.New("invalid address")
} else if address.IsDomain() {
} else if address.IsFqdn() {
return nil, E.New("domain not resolved")
}
if d.networkStrategy == nil {
@@ -332,9 +329,9 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
if !destination.Is6() {
return d.dialer4.Dialer
} else {
return d.dialer6.Dialer
} else {
return d.dialer4.Dialer
}
}

View File

@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
if err != nil {
return nil, err
}
if !destination.IsDomain() {
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if err != nil {
return nil, err
}
if !destination.IsDomain() {
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
if err != nil {
return nil, err
}
if !destination.IsDomain() {
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
if err != nil {
return nil, err
}
if !destination.IsDomain() {
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)

View File

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

View File

@@ -14,7 +14,6 @@ import (
type Searcher interface {
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
Close() error
}
var ErrNotFound = E.New("process not found")
@@ -29,7 +28,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
if err != nil {
return nil, err
}
if info.UserId != -1 && info.UserName == "" {
if info.UserId != -1 {
osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil {
info.UserName = osUser.Username

View File

@@ -6,7 +6,6 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
)
var _ Searcher = (*androidSearcher)(nil)
@@ -19,30 +18,22 @@ func NewSearcher(config Config) (Searcher, error) {
return &androidSearcher{config.PackageManager}, nil
}
func (s *androidSearcher) Close() error {
return nil
}
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
family, protocol, err := socketDiagSettings(network, source)
_, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil {
return nil, err
}
_, uid, err := querySocketDiagOnce(family, protocol, source)
if err != nil {
return nil, err
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageName: sharedPackage,
}, nil
}
appID := uid % 100000
var packageNames []string
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
packageNames = append(packageNames, sharedPackage)
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageName: packageName,
}, nil
}
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
packageNames = append(packageNames, packages...)
}
packageNames = common.Uniq(packageNames)
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageNames: packageNames,
}, nil
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
}

View File

@@ -1,15 +1,19 @@
//go:build darwin
package process
import (
"context"
"encoding/binary"
"net/netip"
"os"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
var _ Searcher = (*darwinSearcher)(nil)
@@ -20,12 +24,12 @@ func NewSearcher(_ Config) (Searcher, error) {
return &darwinSearcher{}, nil
}
func (d *darwinSearcher) Close() error {
return nil
}
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
return FindDarwinConnectionOwner(network, source, destination)
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
if err != nil {
return nil, err
}
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
}
var structSize = func() int {
@@ -43,3 +47,107 @@ var structSize = func() int {
return 384
}
}()
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
case N.NetworkTCP:
spath = "net.inet.tcp.pcblist_n"
case N.NetworkUDP:
spath = "net.inet.udp.pcblist_n"
default:
return "", os.ErrInvalid
}
isIPv4 := ip.Is4()
value, err := unix.SysctlRaw(spath)
if err != nil {
return "", err
}
buf := value
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := structSize
if network == N.NetworkTCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
inp, so := i, i+104
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
if uint16(port) != srcPort {
continue
}
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP netip.Addr
srcIsIPv4 := false
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80]))
srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80]))
default:
continue
}
if ip == srcIP {
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
// udp packet connection may be not equal with srcIP
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
pid := readNativeUint32(buf[so+68 : so+72])
fallbackUDPProcess, _ = getExecPathFromPID(pid)
}
}
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
return fallbackUDPProcess, nil
}
return "", ErrNotFound
}
func getExecPathFromPID(pid uint32) (string, error) {
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return unix.ByteSliceToString(buf), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}

View File

@@ -1,269 +0,0 @@
//go:build darwin
package process
import (
"encoding/binary"
"net/netip"
"os"
"sync"
"syscall"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
const (
darwinSnapshotTTL = 200 * time.Millisecond
darwinXinpgenSize = 24
darwinXsocketOffset = 104
darwinXinpcbForeignPort = 16
darwinXinpcbLocalPort = 18
darwinXinpcbVFlag = 44
darwinXinpcbForeignAddr = 48
darwinXinpcbLocalAddr = 64
darwinXinpcbIPv4Addr = 12
darwinXsocketUID = 64
darwinXsocketLastPID = 68
darwinTCPExtraStructSize = 208
)
type darwinConnectionEntry struct {
localAddr netip.Addr
remoteAddr netip.Addr
localPort uint16
remotePort uint16
pid uint32
uid int32
}
type darwinConnectionMatchKind uint8
const (
darwinConnectionMatchExact darwinConnectionMatchKind = iota
darwinConnectionMatchLocalFallback
darwinConnectionMatchWildcardFallback
)
type darwinSnapshot struct {
createdAt time.Time
entries []darwinConnectionEntry
}
type darwinConnectionFinder struct {
access sync.Mutex
ttl time.Duration
snapshots map[string]darwinSnapshot
builder func(string) (darwinSnapshot, error)
}
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
return &darwinConnectionFinder{
ttl: ttl,
snapshots: make(map[string]darwinSnapshot),
builder: buildDarwinSnapshot,
}
}
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
return sharedDarwinConnectionFinder.find(network, source, destination)
}
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
networkName := N.NetworkName(network)
source = normalizeDarwinAddrPort(source)
destination = normalizeDarwinAddrPort(destination)
var lastOwner *adapter.ConnectionOwner
for attempt := 0; attempt < 2; attempt++ {
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
if err != nil {
return nil, err
}
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
if err != nil {
if err == ErrNotFound && fromCache {
continue
}
return nil, err
}
if fromCache && matchKind != darwinConnectionMatchExact {
continue
}
owner := &adapter.ConnectionOwner{
UserId: entry.uid,
}
lastOwner = owner
if entry.pid == 0 {
return owner, nil
}
processPath, err := getExecPathFromPID(entry.pid)
if err == nil {
owner.ProcessPath = processPath
return owner, nil
}
if fromCache {
continue
}
return owner, nil
}
if lastOwner != nil {
return lastOwner, nil
}
return nil, ErrNotFound
}
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
f.access.Lock()
defer f.access.Unlock()
if !forceRefresh {
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
return snapshot, true, nil
}
}
snapshot, err := f.builder(network)
if err != nil {
return darwinSnapshot{}, false, err
}
f.snapshots[network] = snapshot
return snapshot, false, nil
}
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
spath, itemSize, err := darwinSnapshotSettings(network)
if err != nil {
return darwinSnapshot{}, err
}
value, err := unix.SysctlRaw(spath)
if err != nil {
return darwinSnapshot{}, err
}
return darwinSnapshot{
createdAt: time.Now(),
entries: parseDarwinSnapshot(value, itemSize),
}, nil
}
func darwinSnapshotSettings(network string) (string, int, error) {
itemSize := structSize
switch network {
case N.NetworkTCP:
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
case N.NetworkUDP:
return "net.inet.udp.pcblist_n", itemSize, nil
default:
return "", 0, os.ErrInvalid
}
}
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
inp := i
so := i + darwinXsocketOffset
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
if ok {
entries = append(entries, entry)
}
}
return entries
}
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
return darwinConnectionEntry{}, false
}
entry := darwinConnectionEntry{
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
}
flag := inp[darwinXinpcbVFlag]
switch {
case flag&0x1 != 0:
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
return entry, true
case flag&0x2 != 0:
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
return entry, true
default:
return darwinConnectionEntry{}, false
}
}
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
sourceAddr := source.Addr()
if !sourceAddr.IsValid() {
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
}
var localFallback darwinConnectionEntry
var hasLocalFallback bool
var wildcardFallback darwinConnectionEntry
var hasWildcardFallback bool
for _, entry := range entries {
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
continue
}
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
return entry, darwinConnectionMatchExact, nil
}
if !destination.IsValid() && entry.localAddr == sourceAddr {
return entry, darwinConnectionMatchExact, nil
}
if network != N.NetworkUDP {
continue
}
if !hasLocalFallback && entry.localAddr == sourceAddr {
hasLocalFallback = true
localFallback = entry
}
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
hasWildcardFallback = true
wildcardFallback = entry
}
}
if hasLocalFallback {
return localFallback, darwinConnectionMatchLocalFallback, nil
}
if hasWildcardFallback {
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
}
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
}
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
if !addrPort.IsValid() {
return addrPort
}
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
}
func getExecPathFromPID(pid uint32) (string, error) {
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return unix.ByteSliceToString(buf), nil
}

View File

@@ -4,82 +4,33 @@ package process
import (
"context"
"errors"
"net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
)
var _ Searcher = (*linuxSearcher)(nil)
type linuxSearcher struct {
logger log.ContextLogger
diagConns [4]*socketDiagConn
processPathCache *uidProcessPathCache
logger log.ContextLogger
}
func NewSearcher(config Config) (Searcher, error) {
searcher := &linuxSearcher{
logger: config.Logger,
processPathCache: newUIDProcessPathCache(time.Second),
}
for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} {
for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} {
searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol)
}
}
return searcher, nil
}
func (s *linuxSearcher) Close() error {
var errs []error
for _, conn := range s.diagConns {
if conn == nil {
continue
}
errs = append(errs, conn.Close())
}
return E.Errors(errs...)
return &linuxSearcher{config.Logger}, nil
}
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
inode, uid, err := s.resolveSocketByNetlink(network, source, destination)
inode, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil {
return nil, err
}
processInfo := &adapter.ConnectionOwner{
UserId: int32(uid),
}
processPath, err := s.processPathCache.findProcessPath(inode, uid)
processPath, err := resolveProcessNameByProcSearch(inode, uid)
if err != nil {
s.logger.DebugContext(ctx, "find process path: ", err)
} else {
processInfo.ProcessPath = processPath
}
return processInfo, nil
}
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
family, protocol, err := socketDiagSettings(network, source)
if err != nil {
return 0, 0, err
}
conn := s.diagConns[socketDiagConnIndex(family, protocol)]
if conn == nil {
return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol)
}
if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() {
inode, uid, err = conn.query(source, destination)
if err == nil {
return inode, uid, nil
}
if !errors.Is(err, ErrNotFound) {
return 0, 0, err
}
}
return querySocketDiagOnce(family, protocol, source)
return &adapter.ConnectionOwner{
UserId: int32(uid),
ProcessPath: processPath,
}, nil
}

View File

@@ -3,67 +3,43 @@
package process
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"path"
"strings"
"sync"
"syscall"
"time"
"unicode"
"unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
)
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
var nativeEndian = func() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
return binary.BigEndian
}
return binary.LittleEndian
}()
const (
sizeOfSocketDiagRequestData = 56
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
socketDiagResponseMinSize = 72
socketDiagByFamily = 20
pathProc = "/proc"
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
socketDiagByFamily = 20
pathProc = "/proc"
)
type socketDiagConn struct {
access sync.Mutex
family uint8
protocol uint8
fd int
}
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
var family uint8
var protocol uint8
type uidProcessPathCache struct {
cache freelru.Cache[uint32, *uidProcessPaths]
}
type uidProcessPaths struct {
entries map[uint32]string
}
func newSocketDiagConn(family, protocol uint8) *socketDiagConn {
return &socketDiagConn{
family: family,
protocol: protocol,
fd: -1,
}
}
func socketDiagConnIndex(family, protocol uint8) int {
index := 0
if protocol == syscall.IPPROTO_UDP {
index += 2
}
if family == syscall.AF_INET6 {
index++
}
return index
}
func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) {
switch network {
case N.NetworkTCP:
protocol = syscall.IPPROTO_TCP
@@ -72,308 +48,151 @@ func socketDiagSettings(network string, source netip.AddrPort) (family, protocol
default:
return 0, 0, os.ErrInvalid
}
switch {
case source.Addr().Is4():
if source.Addr().Is4() {
family = syscall.AF_INET
case source.Addr().Is6():
} else {
family = syscall.AF_INET6
default:
return 0, 0, os.ErrInvalid
}
return family, protocol, nil
}
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
cache.SetLifetime(ttl)
return &uidProcessPathCache{cache: cache}
}
req := packSocketDiagRequest(family, protocol, source)
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
if cached, ok := c.cache.Get(uid); ok {
if processPath, found := cached.entries[targetInode]; found {
return processPath, nil
}
}
processPaths, err := buildProcessPathByUIDCache(uid)
if err != nil {
return "", err
}
c.cache.Add(uid, &uidProcessPaths{entries: processPaths})
processPath, found := processPaths[targetInode]
if !found {
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
}
return processPath, nil
}
func (c *socketDiagConn) Close() error {
c.access.Lock()
defer c.access.Unlock()
return c.closeLocked()
}
func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
c.access.Lock()
defer c.access.Unlock()
request := packSocketDiagRequest(c.family, c.protocol, source, destination, false)
for attempt := 0; attempt < 2; attempt++ {
err = c.ensureOpenLocked()
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
inode, uid, err = querySocketDiag(c.fd, request)
if err == nil || errors.Is(err, ErrNotFound) {
return inode, uid, err
}
if !shouldRetrySocketDiag(err) {
return 0, 0, err
}
_ = c.closeLocked()
}
return 0, 0, err
}
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
fd, err := openSocketDiag()
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
defer syscall.Close(fd)
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
}
defer syscall.Close(socket)
func (c *socketDiagConn) ensureOpenLocked() error {
if c.fd != -1 {
return nil
}
fd, err := openSocketDiag()
if err != nil {
return err
}
c.fd = fd
return nil
}
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
func openSocketDiag() (int, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG)
if err != nil {
return -1, err
}
timeout := &syscall.Timeval{Usec: 100}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.Connect(fd, &syscall.SockaddrNetlink{
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pad: 0,
Pid: 0,
Groups: 0,
}); err != nil {
syscall.Close(fd)
return -1, err
})
if err != nil {
return
}
return fd, nil
}
func (c *socketDiagConn) closeLocked() error {
if c.fd == -1 {
return nil
}
err := syscall.Close(c.fd)
c.fd = -1
return err
}
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte {
request := make([]byte, sizeOfSocketDiagRequest)
binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest)
binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily)
flags := uint16(syscall.NLM_F_REQUEST)
if dump {
flags |= syscall.NLM_F_DUMP
}
binary.NativeEndian.PutUint16(request[6:8], flags)
binary.NativeEndian.PutUint32(request[8:12], 0)
binary.NativeEndian.PutUint32(request[12:16], 0)
request[16] = family
request[17] = protocol
request[18] = 0
request[19] = 0
if dump {
binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF)
}
requestSource := source
requestDestination := destination
if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() {
// udp_dump_one expects the exact-match endpoints reversed for historical reasons.
requestSource, requestDestination = destination, source
}
binary.BigEndian.PutUint16(request[24:26], requestSource.Port())
binary.BigEndian.PutUint16(request[26:28], requestDestination.Port())
if family == syscall.AF_INET6 {
copy(request[28:44], requestSource.Addr().AsSlice())
if requestDestination.IsValid() {
copy(request[44:60], requestDestination.Addr().AsSlice())
}
} else {
copy(request[28:32], requestSource.Addr().AsSlice())
if requestDestination.IsValid() {
copy(request[44:48], requestDestination.Addr().AsSlice())
}
}
binary.NativeEndian.PutUint32(request[60:64], 0)
binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF)
return request
}
func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
_, err = syscall.Write(fd, request)
_, err = syscall.Write(socket, req)
if err != nil {
return 0, 0, E.Cause(err, "write netlink request")
}
buffer := make([]byte, 64<<10)
n, err := syscall.Read(fd, buffer)
buffer := buf.New()
defer buffer.Release()
n, err := syscall.Read(socket, buffer.FreeBytes())
if err != nil {
return 0, 0, E.Cause(err, "read netlink response")
}
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
buffer.Truncate(n)
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
if err != nil {
return 0, 0, E.Cause(err, "parse netlink message")
} else if len(messages) == 0 {
return 0, 0, E.New("unexcepted netlink response")
}
return unpackSocketDiagMessages(messages)
message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, E.New("netlink message: NLMSG_ERROR")
}
inode, uid = unpackSocketDiagResponse(&messages[0])
return
}
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
for _, message := range messages {
switch message.Header.Type {
case syscall.NLMSG_DONE:
continue
case syscall.NLMSG_ERROR:
err = unpackSocketDiagError(&message)
if err != nil {
return 0, 0, err
}
case socketDiagByFamily:
inode, uid = unpackSocketDiagResponse(&message)
if inode != 0 || uid != 0 {
return inode, uid, nil
}
}
}
return 0, 0, ErrNotFound
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
s := make([]byte, 16)
copy(s, source.Addr().AsSlice())
buf := make([]byte, sizeOfSocketDiagRequest)
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
nativeEndian.PutUint32(buf[8:12], 0)
nativeEndian.PutUint32(buf[12:16], 0)
buf[16] = family
buf[17] = protocol
buf[18] = 0
buf[19] = 0
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
binary.BigEndian.PutUint16(buf[24:26], source.Port())
binary.BigEndian.PutUint16(buf[26:28], 0)
copy(buf[28:44], s)
copy(buf[44:60], net.IPv6zero)
nativeEndian.PutUint32(buf[60:64], 0)
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
return buf
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < socketDiagResponseMinSize {
if len(msg.Data) < 72 {
return 0, 0
}
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
return inode, uid
data := msg.Data
uid = nativeEndian.Uint32(data[64:68])
inode = nativeEndian.Uint32(data[68:72])
return
}
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
if len(msg.Data) < 4 {
return E.New("netlink message: NLMSG_ERROR")
}
errno := int32(binary.NativeEndian.Uint32(msg.Data[:4]))
if errno == 0 {
return nil
}
if errno < 0 {
errno = -errno
}
sysErr := syscall.Errno(errno)
switch sysErr {
case syscall.ENOENT, syscall.ESRCH:
return ErrNotFound
default:
return E.New("netlink message: ", sysErr)
}
}
func shouldRetrySocketDiag(err error) bool {
return err != nil && !errors.Is(err, ErrNotFound)
}
func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) {
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
files, err := os.ReadDir(pathProc)
if err != nil {
return nil, err
return "", err
}
buffer := make([]byte, syscall.PathMax)
processPaths := make(map[uint32]string)
for _, file := range files {
if !file.IsDir() || !isPid(file.Name()) {
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue
}
info, err := file.Info()
info, err := f.Info()
if err != nil {
if isIgnorableProcError(err) {
continue
}
return nil, err
return "", err
}
if info.Sys().(*syscall.Stat_t).Uid != uid {
continue
}
processPath := filepath.Join(pathProc, file.Name())
fdPath := filepath.Join(processPath, "fd")
exePath, err := os.Readlink(filepath.Join(processPath, "exe"))
if err != nil {
if isIgnorableProcError(err) {
continue
}
return nil, err
}
processPath := path.Join(pathProc, f.Name())
fdPath := path.Join(processPath, "fd")
fds, err := os.ReadDir(fdPath)
if err != nil {
continue
}
for _, fd := range fds {
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer)
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
if err != nil {
continue
}
inode, ok := parseSocketInode(buffer[:n])
if !ok {
continue
}
if _, loaded := processPaths[inode]; !loaded {
processPaths[inode] = exePath
if bytes.Equal(buffer[:n], socket) {
return os.Readlink(path.Join(processPath, "exe"))
}
}
}
return processPaths, nil
}
func isIgnorableProcError(err error) bool {
return os.IsNotExist(err) || os.IsPermission(err)
}
func parseSocketInode(link []byte) (uint32, bool) {
const socketPrefix = "socket:["
if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' {
return 0, false
}
var inode uint64
for _, char := range link[len(socketPrefix) : len(link)-1] {
if char < '0' || char > '9' {
return 0, false
}
inode = inode*10 + uint64(char-'0')
if inode > uint64(^uint32(0)) {
return 0, false
}
}
return uint32(inode), true
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
}
func isPid(s string) bool {

View File

@@ -1,60 +0,0 @@
//go:build linux
package process
import (
"net"
"net/netip"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestQuerySocketDiagUDPExact(t *testing.T) {
t.Parallel()
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer server.Close()
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
require.NoError(t, err)
defer client.Close()
err = client.SetDeadline(time.Now().Add(time.Second))
require.NoError(t, err)
_, err = client.Write([]byte{0})
require.NoError(t, err)
err = server.SetReadDeadline(time.Now().Add(time.Second))
require.NoError(t, err)
buffer := make([]byte, 1)
_, _, err = server.ReadFromUDP(buffer)
require.NoError(t, err)
source := addrPortFromUDPAddr(t, client.LocalAddr())
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
fd, err := openSocketDiag()
require.NoError(t, err)
defer syscall.Close(fd)
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
require.NoError(t, err)
require.NotZero(t, inode)
require.EqualValues(t, os.Getuid(), uid)
}
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
t.Helper()
udpAddr, ok := addr.(*net.UDPAddr)
require.True(t, ok)
ip, ok := netip.AddrFromSlice(udpAddr.IP)
require.True(t, ok)
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
}

View File

@@ -28,10 +28,6 @@ func initWin32API() error {
return winiphlpapi.LoadExtendedTable()
}
func (s *windowsSearcher) Close() error {
return nil
}
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
pid, err := winiphlpapi.FindPid(network, source)
if err != nil {

View File

@@ -38,6 +38,13 @@ const (
TypeURLTest = "urltest"
)
const (
BalancerStrategyLeastUsed = "least-used"
BalancerStrategyRoundRobin = "round-robin"
BalancerStrategyRandom = "random"
BalancerStrategyFallback = "fallback"
)
func ProxyDisplayName(proxyType string) string {
switch proxyType {
case TypeTun:

View File

@@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
s.serviceAccess.Lock()
switch s.serviceStatus.Status {
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
default:
s.serviceAccess.Unlock()
return os.ErrInvalid
@@ -226,14 +226,13 @@ func (s *StartedService) CloseService() error {
return os.ErrInvalid
}
s.updateStatus(ServiceStatus_STOPPING)
instance := s.instance
s.instance = nil
if instance != nil {
err := instance.Close()
if s.instance != nil {
err := s.instance.Close()
if err != nil {
return s.updateStatusError(err)
}
}
s.instance = nil
s.startedAt = time.Time{}
s.updateStatus(ServiceStatus_IDLE)
s.serviceAccess.Unlock()
@@ -950,11 +949,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
var processInfo *ProcessInfo
if metadata.Metadata.ProcessInfo != nil {
processInfo = &ProcessInfo{
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
UserId: metadata.Metadata.ProcessInfo.UserId,
UserName: metadata.Metadata.ProcessInfo.UserName,
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
UserId: metadata.Metadata.ProcessInfo.UserId,
UserName: metadata.Metadata.ProcessInfo.UserName,
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
}
}
return &Connection{

View File

@@ -1460,7 +1460,7 @@ type ProcessInfo struct {
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"`
PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"`
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string {
return ""
}
func (x *ProcessInfo) GetPackageNames() []string {
func (x *ProcessInfo) GetPackageName() string {
if x != nil {
return x.PackageNames
return x.PackageName
}
return nil
return ""
}
type CloseConnectionRequest struct {
@@ -1884,13 +1884,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\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" +
"\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" +
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
"\x16CloseConnectionRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
"\x12DeprecatedWarnings\x125\n" +

View File

@@ -195,7 +195,7 @@ message ProcessInfo {
int32 userId = 2;
string userName = 3;
string processPath = 4;
repeated string packageNames = 5;
string packageName = 5;
}
message CloseConnectionRequest {

View File

@@ -283,9 +283,6 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
@@ -297,9 +294,6 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
@@ -387,21 +381,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
}
if c.disableExpire {
if !c.independentCache {
c.cache.Add(question, message.Copy())
c.cache.Add(question, message)
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy())
}, message)
}
} else {
if !c.independentCache {
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
} else {
c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy(), time.Second*time.Duration(timeToLive))
}, message, time.Second*time.Duration(timeToLive))
}
}
}
@@ -492,9 +486,6 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl)
}
@@ -509,18 +500,12 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = record.Header().Ttl - duration
}
}
} else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = uint32(nowTTL)
}
}

View File

@@ -7,6 +7,7 @@ import (
"strings"
"syscall"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
@@ -39,6 +40,13 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, servers, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = dns.RcodeSuccess
}
}
select {
case results <- queryResult{response, err}:
case <-returned:

View File

@@ -7,6 +7,7 @@ import (
"syscall"
"time"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
@@ -48,6 +49,13 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result")
}
}
select {
case results <- queryResult{response, err}:
case <-returned:

View File

@@ -2,19 +2,7 @@
icon: material/alert-decagram
---
#### 1.13.7
* Fixes and improvements
#### 1.13.6
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.13.4
#### 1.14.0-alpha.3
* Fixes and improvements
@@ -42,6 +30,59 @@ from [SagerNet/go](https://github.com/SagerNet/go).
See [OCM](/configuration/service/ocm).
#### 1.12.24
* Fixes and improvements
#### 1.14.0-alpha.2
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.14.0-alpha.1
* Add `source_mac_address` and `source_hostname` rule items **1**
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
* Update NaiveProxy to 145.0.7632.159 **3**
* Fixes and improvements
**1**:
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
Supported on Linux, macOS, or in graphical clients on Android and macOS.
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
**2**:
Limit or exclude devices from TUN routing by MAC address.
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
See [TUN](/configuration/inbound/tun/#include_mac_address).
**3**:
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
#### 1.13.2
* Fixes and improvements

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.12.0 废弃"
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
### 结构

View File

@@ -2,6 +2,11 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -149,6 +154,12 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -209,7 +220,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound
@@ -408,6 +419,26 @@ Matches network interface (same values as `network_type`) address.
Match default interface address.
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### wifi_ssid
!!! quote ""
@@ -546,4 +577,4 @@ Match any IP with query response.
#### rules
Included rules.
Included rules.

View File

@@ -2,6 +2,11 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -149,6 +154,12 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -208,7 +219,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) &&
`other fields`
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
另外,引用规则集可视为被合并,而不是作为一个单独的规则子项
#### inbound
@@ -256,7 +267,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
匹配 Geosite。
@@ -264,7 +275,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
匹配源 GeoIP。
@@ -407,6 +418,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配默认接口地址。
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### wifi_ssid
!!! quote ""
@@ -453,7 +484,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 废弃"
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
匹配出站。
@@ -505,7 +536,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
与查询响应匹配 GeoIP。
@@ -550,4 +581,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
==必填==
包括的规则。
包括的规则。

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "Deprecated in sing-box 1.12.0"
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
!!! quote "sing-box 1.9.0 中的更改"

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -42,7 +42,7 @@
将拒绝的 DNS 响应缓存存储在缓存文件中。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
#### rdrc_timeout

View File

@@ -1,6 +1,6 @@
!!! quote ""
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#构建标记)。
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#_5)。
### 结构

View File

@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -26,7 +26,7 @@
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### users

View File

@@ -104,4 +104,4 @@ base64 编码的认证密码。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -85,7 +85,7 @@ Hysteria 用户
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### masquerade

View File

@@ -60,4 +60,4 @@ QUIC 拥塞控制算法。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -93,4 +93,4 @@
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。

View File

@@ -43,7 +43,7 @@ Trojan 用户。
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### fallback
@@ -61,7 +61,7 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
#### transport

View File

@@ -75,4 +75,4 @@ QUIC 拥塞控制算法
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -134,6 +134,12 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -560,6 +566,30 @@ Limit android packages in route.
Exclude android packages in route.
#### include_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Limit MAC addresses in route. Not limited by default.
Conflict with `exclude_mac_address`.
#### exclude_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Exclude MAC addresses in route.
Conflict with `include_mac_address`.
#### platform
Platform-specific settings, provided by client applications.

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "sing-box 1.13.3 中的更改"
:material-alert: [strict_route](#strict_route)
@@ -130,6 +135,12 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -543,6 +554,30 @@ TCP/IP 栈。
排除路由的 Android 应用包名。
#### include_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
限制被路由的 MAC 地址。默认不限制。
`exclude_mac_address` 冲突。
#### exclude_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
排除路由的 MAC 地址。
`include_mac_address` 冲突。
#### platform
平台特定的设置,由客户端应用提供。

View File

@@ -48,11 +48,11 @@ VLESS 子协议。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
#### transport

View File

@@ -43,11 +43,11 @@ VMess 用户。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
#### transport

View File

@@ -59,7 +59,7 @@ AnyTLS 密码。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -4,8 +4,8 @@ icon: material/alert-decagram
!!! quote "sing-box 1.11.0 中的更改"
:material-delete-clock: [override_address](#override_address)
:material-delete-clock: [override_port](#override_port)
:material-alert-decagram: [override_address](#override_address)
:material-alert-decagram: [override_port](#override_port)
`direct` 出站直接发送请求。
@@ -29,7 +29,7 @@ icon: material/alert-decagram
!!! failure "已在 sing-box 1.11.0 废弃"
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
覆盖连接目标地址。
@@ -37,7 +37,7 @@ icon: material/alert-decagram
!!! failure "已在 sing-box 1.11.0 废弃"
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
覆盖连接目标端口。

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.11.0 废弃"
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作).
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
`dns` 出站是一个内部 DNS 服务器。

View File

@@ -51,7 +51,7 @@ HTTP 请求的额外标头。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -134,7 +134,7 @@ base64 编码的认证密码。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -105,7 +105,7 @@ QUIC 流量混淆器密码.
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### brutal_debug

View File

@@ -105,7 +105,7 @@ QUIC 拥塞控制算法。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
只有 `server_name``certificate``certificate_path``ech` 是被支持的。

View File

@@ -17,7 +17,7 @@
!!! quote ""
选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。
选择器目前只能通过 [Clash API](/zh/configuration/experimental#clash-api) 来控制。
### 字段

View File

@@ -95,7 +95,7 @@ UDP over TCP 配置。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
### 拨号字段

View File

@@ -49,7 +49,7 @@ ShadowTLS 协议版本。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -18,7 +18,7 @@
!!! info ""
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。
### 字段

View File

@@ -47,11 +47,11 @@ Trojan 密码。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
#### transport

View File

@@ -97,7 +97,7 @@ UDP 包中继模式
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段

View File

@@ -57,7 +57,7 @@ VLESS 子协议。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### packet_encoding
@@ -71,7 +71,7 @@ UDP 包编码,默认使用 xudp。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
#### transport

View File

@@ -82,7 +82,7 @@ VMess 用户 ID。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### packet_encoding
@@ -96,7 +96,7 @@ UDP 包编码。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
#### transport

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.11.0 废弃"
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
!!! quote "sing-box 1.11.0 中的更改"

View File

@@ -4,7 +4,7 @@ icon: material/note-remove
!!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
### 结构

View File

@@ -4,7 +4,7 @@ icon: material/note-remove
!!! failure "已在 sing-box 1.12.0 中被移除"
Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
### 结构

View File

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

View File

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

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -159,6 +164,12 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -199,7 +210,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) &&
`other fields`
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound
@@ -449,6 +460,26 @@ Match specified outbounds' preferred routes.
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### rule_set
!!! question "Since sing-box 1.8.0"

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -22,7 +27,6 @@ icon: material/new-box
:material-plus: [client](#client)
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
:material-plus: [process_path_regex](#process_path_regex)
!!! quote "sing-box 1.8.0 中的更改"
@@ -157,6 +161,12 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -197,7 +207,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) &&
`other fields`
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义
另外,引用规则集可视为被合并,而不是作为一个单独的规则子项
#### inbound
@@ -255,7 +265,7 @@ icon: material/new-box
!!! failure "已在 sing-box 1.8.0 废弃"
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
匹配 Geosite。
@@ -263,7 +273,7 @@ icon: material/new-box
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
匹配源 GeoIP。
@@ -271,7 +281,7 @@ icon: material/new-box
!!! failure "已在 sing-box 1.8.0 废弃"
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
匹配 GeoIP。
@@ -447,6 +457,26 @@ icon: material/new-box
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
| `wireguard` | 匹配对端的 allowed IPs |
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### rule_set
!!! question "自 sing-box 1.8.0 起"
@@ -501,4 +531,4 @@ icon: material/new-box
==必填==
包括的规则。
包括的规则。

View File

@@ -66,7 +66,7 @@ icon: material/new-box
目标出站的标签。
如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
#### route-options 字段
@@ -154,22 +154,22 @@ icon: material/new-box
#### network_strategy
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
仅当出站为 `direct``outbound.bind_interface`, `outbound.inet4_bind_address`
`outbound.inet6_bind_address` 未设置时生效。
#### network_type
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。
详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。
#### fallback_network_type
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。
#### fallback_delay
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
#### udp_disable_domain_unmapping

View File

@@ -10,8 +10,8 @@ icon: material/new-box
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [network_type](#network_type)
:material-plus: [network_is_expensive](#network_is_expensive)
:material-plus: [network_is_constrained](#network_is_constrained)
:material-alert: [network_is_expensive](#network_is_expensive)
:material-alert: [network_is_constrained](#network_is_constrained)
### 结构

View File

@@ -10,6 +10,14 @@ CCM (Claude Code Multiplexer) service is a multiplexing service that allows you
It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable.
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [credentials](#credentials)
:material-alert: [credential_path](#credential_path)
:material-alert: [usages_path](#usages_path)
:material-alert: [users](#users)
:material-alert: [detour](#detour)
### Structure
```json
@@ -19,6 +27,7 @@ It handles OAuth authentication with Claude's API on your local machine while al
... // Listen Fields
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -45,6 +54,106 @@ On macOS, credentials are read from the system keychain first, then fall back to
Refreshed tokens are automatically written back to the same location.
!!! question "Since sing-box 1.14.0"
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
On macOS without an explicit `credential_path`, keychain changes are not watched. Automatic reload only applies to the credential file path.
Conflict with `credentials`.
#### credentials
!!! question "Since sing-box 1.14.0"
List of credential configurations for multi-credential mode.
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
Each credential has a `type` field (`default`, `external`, or `balancer`) and a required `tag` field.
##### Default Credential
```json
{
"tag": "a",
"credential_path": "/path/to/.credentials.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20,
"limit_5h": 0,
"limit_weekly": 0
}
```
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
- `usages_path`: Optional usage tracking file for this credential.
- `detour`: Outbound tag for connecting to the Claude API with this credential.
- `reserve_5h`: Reserve threshold (1-99) for 5-hour window. Credential pauses at (100-N)% utilization. Conflict with `limit_5h`.
- `reserve_weekly`: Reserve threshold (1-99) for weekly window. Credential pauses at (100-N)% utilization. Conflict with `limit_weekly`.
- `limit_5h`: Explicit utilization cap (0-100) for 5-hour window. `0` means unset. Credential pauses when utilization reaches this value. Conflict with `reserve_5h`.
- `limit_weekly`: Explicit utilization cap (0-100) for weekly window. `0` means unset. Credential pauses when utilization reaches this value. Conflict with `reserve_weekly`.
##### Balancer Credential
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"]
}
```
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random` `fallback`. `least_used` will be used by default.
- `credentials`: ==Required== List of default credential tags.
##### Fallback Strategy
```json
{
"tag": "backup",
"type": "balancer",
"strategy": "fallback",
"credentials": ["a", "b"]
}
```
A balancer with `strategy: "fallback"` uses credentials in order. It falls through to the next when the current one is exhausted.
- `credentials`: ==Required== Ordered list of default credential tags.
##### External Credential
```json
{
"tag": "remote",
"type": "external",
"url": "",
"server": "",
"server_port": 0,
"token": "",
"reverse": false,
"detour": "",
"usages_path": ""
}
```
Proxies requests through a remote CCM instance instead of using a local OAuth credential.
- `url`: URL of the remote CCM instance. Omit to create a receiver that only waits for inbound reverse connections.
- `server`: Override server address for dialing, separate from URL hostname.
- `server_port`: Override server port for dialing.
- `token`: ==Required== Authentication token for the remote instance.
- `reverse`: Enable connector mode. Requires `url`. A connector dials out to `/ccm/v1/reverse` on the remote instance and cannot serve local requests directly. When `url` is set without `reverse`, the credential proxies requests through the remote instance normally and prefers an established reverse connection when one is available.
- `detour`: Outbound tag for connecting to the remote instance.
- `usages_path`: Optional usage tracking file.
#### usages_path
Path to the file for storing aggregated API usage statistics.
@@ -60,6 +169,10 @@ Statistics are organized by model, context window (200k standard vs 1M premium),
The statistics file is automatically saved every minute and upon service shutdown.
!!! question "Since sing-box 1.14.0"
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
#### users
List of authorized users for token authentication.
@@ -71,7 +184,10 @@ Object format:
```json
{
"name": "",
"token": ""
"token": "",
"credential": "",
"external_credential": "",
"allow_external_usage": false
}
```
@@ -80,6 +196,12 @@ Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
!!! question "Since sing-box 1.14.0"
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
- `external_credential`: Tag of an external credential used only to rewrite response rate-limit headers with aggregated utilization from this user's other available credentials. It does not control request routing; request selection still comes from `credential` and `allow_external_usage`.
- `allow_external_usage`: Allow this user to use external credentials. `false` by default.
#### headers
Custom HTTP headers to send to the Claude API.
@@ -90,6 +212,10 @@ These headers will override any existing headers with the same name.
Outbound tag for connecting to the Claude API.
!!! question "Since sing-box 1.14.0"
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
@@ -129,3 +255,51 @@ export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
claude
```
### Example with Multiple Credentials
#### Server
```json
{
"services": [
{
"type": "ccm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.claude-a/.credentials.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.claude-b/.credentials.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -10,6 +10,14 @@ CCMClaude Code 多路复用器)服务是一个多路复用服务,允许
它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [credentials](#credentials)
:material-alert: [credential_path](#credential_path)
:material-alert: [usages_path](#usages_path)
:material-alert: [users](#users)
:material-alert: [detour](#detour)
### 结构
```json
@@ -19,6 +27,7 @@ CCMClaude Code 多路复用器)服务是一个多路复用服务,允许
... // 监听字段
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -45,6 +54,106 @@ Claude Code OAuth 凭据文件的路径。
刷新的令牌会自动写回相同位置。
!!! question "自 sing-box 1.14.0 起"
`credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
在 macOS 上如果未显式设置 `credential_path`,不会监听钥匙串变化。自动重载只作用于凭据文件路径。
`credentials` 冲突。
#### credentials
!!! question "自 sing-box 1.14.0 起"
多凭据模式的凭据配置列表。
设置后,顶层 `credential_path``usages_path``detour` 被禁止。每个用户必须指定 `credential` 标签。
每个凭据有一个 `type` 字段(`default``external``balancer`)和一个必填的 `tag` 字段。
##### 默认凭据
```json
{
"tag": "a",
"credential_path": "/path/to/.credentials.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20,
"limit_5h": 0,
"limit_weekly": 0
}
```
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
- `usages_path`:此凭据的可选使用跟踪文件。
- `detour`:此凭据用于连接 Claude API 的出站标签。
- `reserve_5h`5 小时窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。与 `limit_5h` 冲突。
- `reserve_weekly`每周窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。与 `limit_weekly` 冲突。
- `limit_5h`5 小时窗口的显式利用率上限0-100`0` 表示未设置显式上限。凭据在利用率达到此值时暂停。与 `reserve_5h` 冲突。
- `limit_weekly`每周窗口的显式利用率上限0-100`0` 表示未设置显式上限。凭据在利用率达到此值时暂停。与 `reserve_weekly` 冲突。
##### 均衡凭据
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"]
}
```
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random` `fallback`。默认使用 `least_used`
- `credentials`==必填== 默认凭据标签列表。
##### 回退策略
```json
{
"tag": "backup",
"type": "balancer",
"strategy": "fallback",
"credentials": ["a", "b"]
}
```
`strategy` 设为 `fallback` 的均衡凭据会按顺序使用凭据。当前凭据耗尽后切换到下一个。
- `credentials`==必填== 有序的默认凭据标签列表。
##### 外部凭据
```json
{
"tag": "remote",
"type": "external",
"url": "",
"server": "",
"server_port": 0,
"token": "",
"reverse": false,
"detour": "",
"usages_path": ""
}
```
通过远程 CCM 实例代理请求,而非使用本地 OAuth 凭据。
- `url`:远程 CCM 实例的 URL。省略时此凭据作为仅等待入站反向连接的接收器。
- `server`:覆盖拨号的服务器地址,与 URL 主机名分开。
- `server_port`:覆盖拨号的服务器端口。
- `token`==必填== 远程实例的身份验证令牌。
- `reverse`:启用连接器模式。要求设置 `url`。启用后,此凭据会主动拨出到远程实例的 `/ccm/v1/reverse`,且不能直接为本地请求提供服务。当设置了 `url` 但未启用 `reverse` 时,此凭据会正常通过远程实例转发请求,并在反向连接建立后优先使用该反向连接。
- `detour`:用于连接远程实例的出站标签。
- `usages_path`:可选的使用跟踪文件。
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
@@ -60,6 +169,10 @@ Claude Code OAuth 凭据文件的路径。
统计文件每分钟自动保存一次,并在服务关闭时保存。
!!! question "自 sing-box 1.14.0 起"
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`
#### users
用于令牌身份验证的授权用户列表。
@@ -71,7 +184,10 @@ Claude Code OAuth 凭据文件的路径。
```json
{
"name": "",
"token": ""
"token": "",
"credential": "",
"external_credential": "",
"allow_external_usage": false
}
```
@@ -80,6 +196,12 @@ Claude Code OAuth 凭据文件的路径。
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
!!! question "自 sing-box 1.14.0 起"
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
- `external_credential`:仅用于用此用户其他可用凭据的聚合利用率重写响应速率限制头的外部凭据标签。它不参与请求路由;请求选择仍由 `credential``allow_external_usage` 决定。
- `allow_external_usage`:允许此用户使用外部凭据。默认为 `false`
#### headers
发送到 Claude API 的自定义 HTTP 头。
@@ -90,9 +212,13 @@ Claude Code OAuth 凭据文件的路径。
用于连接 Claude API 的出站标签。
!!! question "自 sing-box 1.14.0 起"
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
### 示例
@@ -129,3 +255,51 @@ export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
claude
```
### 多凭据示例
#### 服务端
```json
{
"services": [
{
"type": "ccm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.claude-a/.credentials.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.claude-b/.credentials.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "ak-ccm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "ak-ccm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -36,7 +36,7 @@ DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.g
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### config_path
@@ -96,7 +96,7 @@ Derper 配置文件路径。
- `server`**必填** DERP 服务器地址。
- `server_port`**必填** DERP 服务器端口。
- `host`:自定义 DERP 主机名。
- `tls`[TLS](/zh/configuration/shared/tls/#出站)
- `tls`[TLS](/zh/configuration/shared/tls/#outbound)
- `拨号字段`[拨号字段](/zh/configuration/shared/dial/)
#### mesh_psk

View File

@@ -10,6 +10,14 @@ OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you
It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens.
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [credentials](#credentials)
:material-alert: [credential_path](#credential_path)
:material-alert: [usages_path](#usages_path)
:material-alert: [users](#users)
:material-alert: [detour](#detour)
### Structure
```json
@@ -19,6 +27,7 @@ It handles OAuth authentication with OpenAI's API on your local machine while al
... // Listen Fields
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -43,6 +52,104 @@ If not specified, defaults to:
Refreshed tokens are automatically written back to the same location.
!!! question "Since sing-box 1.14.0"
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
Conflict with `credentials`.
#### credentials
!!! question "Since sing-box 1.14.0"
List of credential configurations for multi-credential mode.
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
Each credential has a `type` field (`default`, `external`, or `balancer`) and a required `tag` field.
##### Default Credential
```json
{
"tag": "a",
"credential_path": "/path/to/auth.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20,
"limit_5h": 0,
"limit_weekly": 0
}
```
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
- `usages_path`: Optional usage tracking file for this credential.
- `detour`: Outbound tag for connecting to the OpenAI API with this credential.
- `reserve_5h`: Reserve threshold (1-99) for primary rate limit window. Credential pauses at (100-N)% utilization. Conflict with `limit_5h`.
- `reserve_weekly`: Reserve threshold (1-99) for secondary (weekly) rate limit window. Credential pauses at (100-N)% utilization. Conflict with `limit_weekly`.
- `limit_5h`: Explicit utilization cap (0-100) for primary rate limit window. `0` means unset. Credential pauses when utilization reaches this value. Conflict with `reserve_5h`.
- `limit_weekly`: Explicit utilization cap (0-100) for secondary (weekly) rate limit window. `0` means unset. Credential pauses when utilization reaches this value. Conflict with `reserve_weekly`.
##### Balancer Credential
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"]
}
```
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random` `fallback`. `least_used` will be used by default.
- `credentials`: ==Required== List of default credential tags.
##### Fallback Strategy
```json
{
"tag": "backup",
"type": "balancer",
"strategy": "fallback",
"credentials": ["a", "b"]
}
```
A balancer with `strategy: "fallback"` uses credentials in order. It falls through to the next when the current one is exhausted.
- `credentials`: ==Required== Ordered list of default credential tags.
##### External Credential
```json
{
"tag": "remote",
"type": "external",
"url": "",
"server": "",
"server_port": 0,
"token": "",
"reverse": false,
"detour": "",
"usages_path": ""
}
```
Proxies requests through a remote OCM instance instead of using a local OAuth credential.
- `url`: URL of the remote OCM instance. Omit to create a receiver that only waits for inbound reverse connections.
- `server`: Override server address for dialing, separate from URL hostname.
- `server_port`: Override server port for dialing.
- `token`: ==Required== Authentication token for the remote instance.
- `reverse`: Enable connector mode. Requires `url`. A connector dials out to `/ocm/v1/reverse` on the remote instance and cannot serve local requests directly. When `url` is set without `reverse`, the credential proxies requests through the remote instance normally and prefers an established reverse connection when one is available.
- `detour`: Outbound tag for connecting to the remote instance.
- `usages_path`: Optional usage tracking file.
#### usages_path
Path to the file for storing aggregated API usage statistics.
@@ -58,6 +165,10 @@ Statistics are organized by model and optionally by user when authentication is
The statistics file is automatically saved every minute and upon service shutdown.
!!! question "Since sing-box 1.14.0"
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
#### users
List of authorized users for token authentication.
@@ -69,7 +180,10 @@ Object format:
```json
{
"name": "",
"token": ""
"token": "",
"credential": "",
"external_credential": "",
"allow_external_usage": false
}
```
@@ -78,6 +192,12 @@ Object fields:
- `name`: Username identifier for tracking purposes.
- `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer <token>` header.
!!! question "Since sing-box 1.14.0"
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
- `external_credential`: Tag of an external credential used only to rewrite response rate-limit headers with aggregated utilization from this user's other available credentials. It does not control request routing; request selection still comes from `credential` and `allow_external_usage`.
- `allow_external_usage`: Allow this user to use external credentials. `false` by default.
#### headers
Custom HTTP headers to send to the OpenAI API.
@@ -88,6 +208,10 @@ These headers will override any existing headers with the same name.
Outbound tag for connecting to the OpenAI API.
!!! question "Since sing-box 1.14.0"
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
@@ -183,3 +307,51 @@ Then run:
```bash
codex --profile ocm
```
### Example with Multiple Credentials
#### Server
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.codex-a/auth.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.codex-b/auth.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -10,6 +10,14 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [credentials](#credentials)
:material-alert: [credential_path](#credential_path)
:material-alert: [usages_path](#usages_path)
:material-alert: [users](#users)
:material-alert: [detour](#detour)
### 结构
```json
@@ -19,6 +27,7 @@ OCMOpenAI Codex 多路复用器)服务是一个多路复用服务,允许
... // 监听字段
"credential_path": "",
"credentials": [],
"usages_path": "",
"users": [],
"headers": {},
@@ -43,6 +52,104 @@ OpenAI OAuth 凭据文件的路径。
刷新的令牌会自动写回相同位置。
!!! question "自 sing-box 1.14.0 起"
`credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
`credentials` 冲突。
#### credentials
!!! question "自 sing-box 1.14.0 起"
多凭据模式的凭据配置列表。
设置后,顶层 `credential_path``usages_path``detour` 被禁止。每个用户必须指定 `credential` 标签。
每个凭据有一个 `type` 字段(`default``external``balancer`)和一个必填的 `tag` 字段。
##### 默认凭据
```json
{
"tag": "a",
"credential_path": "/path/to/auth.json",
"usages_path": "/path/to/usages.json",
"detour": "",
"reserve_5h": 20,
"reserve_weekly": 20,
"limit_5h": 0,
"limit_weekly": 0
}
```
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
- `usages_path`:此凭据的可选使用跟踪文件。
- `detour`:此凭据用于连接 OpenAI API 的出站标签。
- `reserve_5h`主要速率限制窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。与 `limit_5h` 冲突。
- `reserve_weekly`次要每周速率限制窗口的保留阈值1-99。凭据在利用率达到 (100-N)% 时暂停。与 `limit_weekly` 冲突。
- `limit_5h`主要速率限制窗口的显式利用率上限0-100`0` 表示未设置显式上限。凭据在利用率达到此值时暂停。与 `reserve_5h` 冲突。
- `limit_weekly`次要每周速率限制窗口的显式利用率上限0-100`0` 表示未设置显式上限。凭据在利用率达到此值时暂停。与 `reserve_weekly` 冲突。
##### 均衡凭据
```json
{
"tag": "pool",
"type": "balancer",
"strategy": "",
"credentials": ["a", "b"]
}
```
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random` `fallback`。默认使用 `least_used`
- `credentials`==必填== 默认凭据标签列表。
##### 回退策略
```json
{
"tag": "backup",
"type": "balancer",
"strategy": "fallback",
"credentials": ["a", "b"]
}
```
`strategy` 设为 `fallback` 的均衡凭据会按顺序使用凭据。当前凭据耗尽后切换到下一个。
- `credentials`==必填== 有序的默认凭据标签列表。
##### 外部凭据
```json
{
"tag": "remote",
"type": "external",
"url": "",
"server": "",
"server_port": 0,
"token": "",
"reverse": false,
"detour": "",
"usages_path": ""
}
```
通过远程 OCM 实例代理请求,而非使用本地 OAuth 凭据。
- `url`:远程 OCM 实例的 URL。省略时此凭据作为仅等待入站反向连接的接收器。
- `server`:覆盖拨号的服务器地址,与 URL 主机名分开。
- `server_port`:覆盖拨号的服务器端口。
- `token`==必填== 远程实例的身份验证令牌。
- `reverse`:启用连接器模式。要求设置 `url`。启用后,此凭据会主动拨出到远程实例的 `/ocm/v1/reverse`,且不能直接为本地请求提供服务。当设置了 `url` 但未启用 `reverse` 时,此凭据会正常通过远程实例转发请求,并在反向连接建立后优先使用该反向连接。
- `detour`:用于连接远程实例的出站标签。
- `usages_path`:可选的使用跟踪文件。
#### usages_path
用于存储聚合 API 使用统计信息的文件路径。
@@ -58,6 +165,10 @@ OpenAI OAuth 凭据文件的路径。
统计文件每分钟自动保存一次,并在服务关闭时保存。
!!! question "自 sing-box 1.14.0 起"
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`
#### users
用于令牌身份验证的授权用户列表。
@@ -69,7 +180,10 @@ OpenAI OAuth 凭据文件的路径。
```json
{
"name": "",
"token": ""
"token": "",
"credential": "",
"external_credential": "",
"allow_external_usage": false
}
```
@@ -78,6 +192,12 @@ OpenAI OAuth 凭据文件的路径。
- `name`:用于跟踪的用户名标识符。
- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer <token>` 头进行身份验证。
!!! question "自 sing-box 1.14.0 起"
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
- `external_credential`:仅用于用此用户其他可用凭据的聚合利用率重写响应速率限制头的外部凭据标签。它不参与请求路由;请求选择仍由 `credential``allow_external_usage` 决定。
- `allow_external_usage`:允许此用户使用外部凭据。默认为 `false`
#### headers
发送到 OpenAI API 的自定义 HTTP 头。
@@ -88,9 +208,13 @@ OpenAI OAuth 凭据文件的路径。
用于连接 OpenAI API 的出站标签。
!!! question "自 sing-box 1.14.0 起"
`credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
### 示例
@@ -184,3 +308,51 @@ model_provider = "ocm"
```bash
codex --profile ocm
```
### 多凭据示例
#### 服务端
```json
{
"services": [
{
"type": "ocm",
"listen": "0.0.0.0",
"listen_port": 8080,
"credentials": [
{
"tag": "a",
"credential_path": "/home/user/.codex-a/auth.json",
"usages_path": "/data/usages-a.json",
"reserve_5h": 20,
"reserve_weekly": 20
},
{
"tag": "b",
"credential_path": "/home/user/.codex-b/auth.json",
"reserve_5h": 10,
"reserve_weekly": 10
},
{
"tag": "pool",
"type": "balancer",
"credentials": ["a", "b"]
}
],
"users": [
{
"name": "alice",
"token": "sk-ocm-hello-world",
"credential": "pool"
},
{
"name": "bob",
"token": "sk-ocm-hello-bob",
"credential": "a"
}
]
}
]
}
```

View File

@@ -55,4 +55,4 @@ SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务
#### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -173,7 +173,7 @@ TCP keep alive 间隔。
用于设置解析域名的域名解析器。
此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
此选项的格式与 [路由 DNS 规则动作](/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。
若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。
@@ -246,7 +246,7 @@ TCP keep alive 间隔。
!!! failure "已在 sing-box 1.12.0 废弃"
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移出站域名策略选项到域名解析器)。
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver)。
可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`

View File

@@ -145,13 +145,13 @@ UDP NAT 过期时间。
如果设置,连接将被转发到指定的入站。
需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#字段)。
需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#_3)。
#### sniff
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
启用协议探测。
@@ -171,7 +171,7 @@ UDP NAT 过期时间。
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
探测超时时间。
@@ -181,7 +181,7 @@ UDP NAT 过期时间。
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
@@ -193,7 +193,7 @@ UDP NAT 过期时间。
!!! failure "已在 sing-box 1.11.0 废弃"
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作).
入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions).
如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。

View File

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

View File

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

View File

@@ -22,13 +22,13 @@ icon: material/new-box
以 TCP RST / ICMP 不可达拒绝。
详情参阅 [reject](/zh/configuration/route/rule_action/#reject)。
详情参阅 [reject](/configuration/route/rule_action/#reject)。
#### route
将 ICMP 连接路由到指定出站以直接回复。
详情参阅 [route](/zh/configuration/route/rule_action/#route)。
详情参阅 [route](/configuration/route/rule_action/#route)。
#### bypass
@@ -44,4 +44,4 @@ icon: material/new-box
对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。
详情参阅 [bypass](/zh/configuration/route/rule_action/#bypass)。
详情参阅 [bypass](/configuration/route/rule_action/#bypass)。

View File

@@ -426,7 +426,7 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/zh/configuration/inbound/naive/)。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。

View File

@@ -144,7 +144,7 @@ HTTP 请求的额外标头
!!! note ""
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。
```json
{

View File

@@ -4,7 +4,7 @@ icon: material/new-box
# Wi-Fi 状态
!!! quote "sing-box 1.13.0 中的更改"
!!! quote "sing-box 1.13.0 的变更"
:material-plus: Linux 支持
:material-plus: Windows 支持

View File

@@ -7,7 +7,7 @@ icon: material/delete-alert
#### 旧的 DNS 服务器格式
DNS 服务器已重构,
参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式).
参阅 [迁移指南](/migration/#migrate-to-new-dns-servers).
对旧格式的兼容性将在 sing-box 1.14.0 中被移除。
@@ -15,7 +15,7 @@ DNS 服务器已重构,
旧的 `outbound` DNS 规则已废弃,
且可被拨号字段代替,
参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项).
参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
#### 旧的 ECH 字段
@@ -31,28 +31,28 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支
#### 旧的特殊出站
旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。
参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions)。
旧字段将在 sing-box 1.13.0 中被移除。
#### 旧的入站字段
旧的入站字段(`inbound.<sniff/domain_strategy/...>`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。
参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions)。
旧字段将在 sing-box 1.13.0 中被移除。
#### direct 出站中的目标地址覆盖字段
direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
旧字段将在 sing-box 1.13.0 中被移除。
#### WireGuard 出站
WireGuard 出站已废弃且可以通过端点替代,
参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
旧出站将在 sing-box 1.13.0 中被移除。
@@ -86,7 +86,7 @@ GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用
#### Clash API 中的 Cache file 及相关功能
Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置,
参阅 [迁移指南](/zh/migration/#将缓存文件从-clash-api-迁移到独立选项)。
参阅 [迁移指南](/zh/migration/#clash-api)。
#### GeoIP
@@ -96,7 +96,7 @@ maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量
且现有的实现均存在内存使用大与管理困难的问题。
sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
可以完全替代 GeoIP 参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
可以完全替代 GeoIP 参阅 [迁移指南](/zh/migration/#geoip)。
#### Geosite
@@ -106,7 +106,7 @@ Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流
存在着包括缺少维护、规则不准确和管理困难内的大量问题。
sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
可以完全替代 Geosite参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
可以完全替代 Geosite参阅 [迁移指南](/zh/migration/#geosite)。
## 1.6.0

View File

@@ -51,20 +51,20 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| 构建标记 | 默认启动 | 说明 |
|------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/zh/configuration/dns/server/), [Naive inbound](/zh/configuration/inbound/naive/), [Hysteria Inbound](/zh/configuration/inbound/hysteria/), [Hysteria Outbound](/zh/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/zh/configuration/shared/v2ray-transport#quic). |
| `with_grpc` | :material-close: | Build with standard gRPC support, see [V2Ray Transport#gRPC](/zh/configuration/shared/v2ray-transport#grpc). |
| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/zh/configuration/dns/server/). |
| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/zh/configuration/outbound/wireguard/). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/zh/configuration/shared/tls#utls). |
| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/zh/configuration/shared/tls/). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/zh/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/zh/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/zh/configuration/inbound/tun#stack) and [WireGuard outbound](/zh/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/zh/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/zh/configuration/endpoint/tailscale)。 |
| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). |
| `with_grpc` | :material-close: | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). |
| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). |
| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/configuration/endpoint/tailscale)。 |
| `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 |
| `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 |
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 |
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/configuration/outbound/naive/)。 |
| `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API且在外部重新实现不切实际。用于 kTLS内核 TLS 卸载)和原始 TLS 记录操作。 |
| `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 |

View File

@@ -518,9 +518,9 @@ DNS 服务器已经重构。
!!! info "参考"
[DNS 规则](/zh/configuration/dns/rule/#outbound) /
[拨号字段](/zh/configuration/shared/dial/#domain_resolver) /
[路由](/zh/configuration/route/#default_domain_resolver)
[DNS 规则](/configuration/dns/rule/#outbound) /
[拨号字段](/configuration/shared/dial/#domain_resolver) /
[路由](/configuration/route/#default_domain_resolver)
=== ":material-card-remove: 废弃的"
@@ -596,7 +596,7 @@ DNS 服务器已经重构。
!!! info "参考"
[拨号字段](/zh/configuration/shared/dial/#domain_strategy)
[拨号字段](/configuration/shared/dial/#domain_strategy)
=== ":material-card-remove: 弃用的"

View File

@@ -45,8 +45,8 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
if t.Metadata.ProcessInfo != nil {
if t.Metadata.ProcessInfo.ProcessPath != "" {
processPath = t.Metadata.ProcessInfo.ProcessPath
} else if len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 {
processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0]
} else if t.Metadata.ProcessInfo.AndroidPackageName != "" {
processPath = t.Metadata.ProcessInfo.AndroidPackageName
}
if processPath == "" {
if t.Metadata.ProcessInfo.UserId != -1 {

View File

@@ -239,15 +239,11 @@ func (c *Connections) Iterator() ConnectionIterator {
}
type ProcessInfo struct {
ProcessID int64
UserID int32
UserName string
ProcessPath string
packageNames []string
}
func (p *ProcessInfo) PackageNames() StringIterator {
return newIterator(p.packageNames)
ProcessID int64
UserID int32
UserName string
ProcessPath string
PackageName string
}
type Connection struct {
@@ -343,11 +339,11 @@ func connectionFromGRPC(conn *daemon.Connection) Connection {
var processInfo *ProcessInfo
if conn.ProcessInfo != nil {
processInfo = &ProcessInfo{
ProcessID: int64(conn.ProcessInfo.ProcessId),
UserID: conn.ProcessInfo.UserId,
UserName: conn.ProcessInfo.UserName,
ProcessPath: conn.ProcessInfo.ProcessPath,
packageNames: conn.ProcessInfo.PackageNames,
ProcessID: int64(conn.ProcessInfo.ProcessId),
UserID: conn.ProcessInfo.UserId,
UserName: conn.ProcessInfo.UserName,
ProcessPath: conn.ProcessInfo.ProcessPath,
PackageName: conn.ProcessInfo.PackageName,
}
}
return Connection{

View File

@@ -144,6 +144,18 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
return nil
}
func (s *platformInterfaceStub) UsePlatformNeighborResolver() bool {
return false
}
func (s *platformInterfaceStub) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return os.ErrInvalid
}
func (s *platformInterfaceStub) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return nil
}
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
return false
}

View File

@@ -1,57 +0,0 @@
package libbox
import (
"net/netip"
"os/user"
"syscall"
"github.com/sagernet/sing-box/common/process"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) {
source, err := parseConnectionOwnerAddrPort(sourceAddress, sourcePort)
if err != nil {
return nil, E.Cause(err, "parse source")
}
destination, err := parseConnectionOwnerAddrPort(destinationAddress, destinationPort)
if err != nil {
return nil, E.Cause(err, "parse destination")
}
var network string
switch ipProtocol {
case syscall.IPPROTO_TCP:
network = "tcp"
case syscall.IPPROTO_UDP:
network = "udp"
default:
return nil, E.New("unknown protocol: ", ipProtocol)
}
owner, err := process.FindDarwinConnectionOwner(network, source, destination)
if err != nil {
return nil, err
}
result := &ConnectionOwner{
UserId: owner.UserId,
ProcessPath: owner.ProcessPath,
}
if owner.UserId != -1 && owner.UserName == "" {
osUser, _ := user.LookupId(F.ToString(owner.UserId))
if osUser != nil {
result.UserName = osUser.Username
}
}
return result, nil
}
func parseConnectionOwnerAddrPort(address string, port int32) (netip.AddrPort, error) {
if port < 0 || port > 65535 {
return netip.AddrPort{}, E.New("invalid port: ", port)
}
addr, err := netip.ParseAddr(address)
if err != nil {
return netip.AddrPort{}, err
}
return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil
}

View File

@@ -52,11 +52,6 @@ type HTTPRequest interface {
type HTTPResponse interface {
GetContent() (*StringBox, error)
WriteTo(path string) error
WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error
}
type HTTPResponseWriteToProgressHandler interface {
Update(progress int64, total int64)
}
var (
@@ -244,31 +239,3 @@ func (h *httpResponse) WriteTo(path string) error {
defer file.Close()
return common.Error(bufio.Copy(file, h.Body))
}
func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error {
defer h.Body.Close()
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return common.Error(bufio.Copy(&progressWriter{
writer: file,
handler: handler,
total: h.ContentLength,
}, h.Body))
}
type progressWriter struct {
writer io.Writer
handler HTTPResponseWriteToProgressHandler
total int64
written int64
}
func (w *progressWriter) Write(p []byte) (int, error) {
n, err := w.writer.Write(p)
w.written += int64(n)
w.handler.Update(w.written, w.total)
return n, err
}

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