Clash API
The Clash API provides a RESTful HTTP interface compatible with Clash dashboard UIs (e.g., Yacd, Metacubexd). It exposes proxy management, connection tracking, traffic statistics, configuration, log streaming, and DNS cache operations.
Source: experimental/clashapi/
Registration
The Clash API server registers itself via an init() function, guarded by the with_clash_api build tag:
// clashapi.go (with_clash_api build tag)
func init() {
experimental.RegisterClashServerConstructor(NewServer)
}
// clashapi_stub.go (!with_clash_api build tag)
func init() {
experimental.RegisterClashServerConstructor(func(...) (adapter.ClashServer, error) {
return nil, E.New(`clash api is not included in this build, rebuild with -tags with_clash_api`)
})
}Server Architecture
type Server struct {
ctx context.Context
router adapter.Router
dnsRouter adapter.DNSRouter
outbound adapter.OutboundManager
endpoint adapter.EndpointManager
logger log.Logger
httpServer *http.Server
trafficManager *trafficontrol.Manager
urlTestHistory adapter.URLTestHistoryStorage
mode string
modeList []string
modeUpdateHook *observable.Subscriber[struct{}]
externalController bool
externalUI string
externalUIDownloadURL string
externalUIDownloadDetour string
}HTTP Router (chi)
The server uses go-chi/chi for routing with CORS middleware:
GET / -> hello (or redirect to /ui/)
GET /logs -> WebSocket/SSE log streaming
GET /traffic -> WebSocket/SSE traffic statistics
GET /version -> {"version": "sing-box X.Y.Z", "premium": true, "meta": true}
/configs -> GET, PUT, PATCH configuration
/proxies -> GET list, GET/PUT individual proxy, GET delay test
/rules -> GET routing rules
/connections -> GET list, DELETE close all, DELETE close by ID
/providers/proxies -> proxy providers (stub)
/providers/rules -> rule providers (stub)
/script -> script (stub)
/profile -> profile (stub)
/cache -> cache operations
/dns -> DNS operations
/ui/* -> static file server for external UIAuthentication
Bearer token authentication via the secret configuration option:
func authentication(serverSecret string) func(next http.Handler) http.Handler {
// Checks "Authorization: Bearer <token>" header
// WebSocket connections can use ?token=<token> query parameter
// If serverSecret is empty, all requests are allowed
}Connection Tracking
Traffic Manager
type Manager struct {
uploadTotal atomic.Int64
downloadTotal atomic.Int64
connections compatible.Map[uuid.UUID, Tracker]
closedConnectionsAccess sync.Mutex
closedConnections list.List[TrackerMetadata] // capped at 1000
memory uint64
eventSubscriber *observable.Subscriber[ConnectionEvent]
}The manager tracks:
- Global upload/download totals via atomic counters
- Active connections in a concurrent map keyed by UUID
- Recently closed connections in a capped list (max 1000 entries)
- Memory usage via
runtime.ReadMemStats
Tracker Wrapping
When a connection is routed, the Clash server wraps it with a tracking layer:
func (s *Server) RoutedConnection(ctx, conn, metadata, matchedRule, matchOutbound) net.Conn {
return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, ...)
}The tracker:
- Generates a UUID v4 for the connection
- Resolves the outbound chain (follows group selections to find the final outbound)
- Wraps the connection with
bufio.NewCounterConnto count bytes in both directions - Registers with the manager via
manager.Join(tracker) - On close, calls
manager.Leave(tracker)and stores metadata in the closed connections list
TrackerMetadata JSON
Connection metadata is serialized for the API:
func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"id": t.ID,
"metadata": map[string]any{
"network": t.Metadata.Network,
"type": inbound, // "inboundType/inboundTag"
"sourceIP": source.Addr,
"destinationIP": dest.Addr,
"sourcePort": source.Port,
"destinationPort": dest.Port,
"host": domain,
"dnsMode": "normal",
"processPath": processPath,
},
"upload": t.Upload.Load(),
"download": t.Download.Load(),
"start": t.CreatedAt,
"chains": t.Chain, // reversed outbound chain
"rule": rule,
"rulePayload": "",
})
}Traffic Streaming
The /traffic endpoint streams per-second traffic deltas via WebSocket or chunked HTTP:
func traffic(ctx, trafficManager) http.HandlerFunc {
// Every 1 second:
// 1. Read current total upload/download
// 2. Compute delta from previous reading
// 3. Send JSON: {"up": delta_up, "down": delta_down}
}Log Streaming
The /logs endpoint streams log entries with level filtering:
func getLogs(logFactory) http.HandlerFunc {
// Accepts ?level=info|debug|warn|error
// Subscribes to the observable log factory
// Streams JSON: {"type": "info", "payload": "log message"}
// Supports both WebSocket and chunked HTTP
}Mode Switching
sing-box implements Clash-style mode switching (Rule, Global, Direct, etc.):
func (s *Server) SetMode(newMode string) {
// 1. Validate mode is in modeList (case-insensitive)
// 2. Update s.mode
// 3. Emit mode update hook (notifies subscribers)
// 4. Clear DNS cache
// 5. Persist to cache file
// 6. Log the change
}Modes are persisted in the bbolt cache file under the clash_mode bucket, keyed by cache ID.
Proxy Management
GET /proxies
Returns all outbounds and endpoints with their metadata:
func proxyInfo(server, detour) *badjson.JSONObject {
// type: Clash display name (e.g., "Shadowsocks", "VMess")
// name: outbound tag
// udp: whether UDP is supported
// history: URL test delay history
// now: current selection (for groups)
// all: available members (for groups)
}A synthetic GLOBAL proxy group is always added, containing all non-system outbounds with the default outbound listed first.
PUT /proxies/
Updates the selected outbound for Selector groups:
func updateProxy(w, r) {
selector, ok := proxy.(*group.Selector)
selector.SelectOutbound(req.Name)
}GET /proxies/{name}/delay
Performs a URL test with configurable timeout:
func getProxyDelay(server) http.HandlerFunc {
// Reads ?url=...&timeout=... query parameters
// Calls urltest.URLTest(ctx, url, proxy)
// Returns {"delay": ms} or error
// Stores result in URL test history
}Provider Interface
Proxy providers (/providers/proxies) and rule providers (/providers/rules) are stubbed out -- they return empty results or 404. This maintains API compatibility with Clash dashboards that expect these endpoints to exist.
Snapshot API
The /connections endpoint returns a snapshot of all active connections:
type Snapshot struct {
Download int64
Upload int64
Connections []Tracker
Memory uint64 // from runtime.MemStats
}The snapshot endpoint also supports WebSocket for real-time updates with a configurable polling interval (?interval=1000 in milliseconds).
Configuration
{
"experimental": {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "ui",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "my-secret",
"default_mode": "Rule",
"access_control_allow_origin": ["*"],
"access_control_allow_private_network": false
}
}
}Start Lifecycle
The server starts in two phases:
StartStateStart: Loads persisted mode from cache fileStartStateStarted: Downloads external UI if needed, starts the HTTP listener (with retry logic for AndroidEADDRINUSE)
Reimplementation Notes
- The API is designed for compatibility with Clash dashboards (Yacd, Metacubexd). The response format must exactly match what these dashboards expect
- WebSocket support is critical -- traffic, logs, and connections all use WebSocket for real-time streaming
- The
"premium": true, "meta": trueversion response flags enable additional features in dashboards - Connection tracking wraps every routed connection/packet connection, adding per-connection byte counters
- The closed connections list is bounded to 1000 entries (FIFO eviction)
- Memory statistics come from
runtime.ReadMemStatswhich includes stack, heap in use, and idle heap - DNS operations and cache clearing are exposed through the
/dnsand/cacheroutes