How ClashApiManager Works for Clash‑Compatible Clients in v2rayN

The ClashApiManager class is a singleton HTTP wrapper that enables v2rayN to control any Clash‑compatible core via its REST API, exposing async methods to fetch proxy groups, test node latency, switch active selectors, and manage runtime configuration.

The ClashApiManager serves as the central bridge between v2rayN’s UI and locally‑running Clash cores. Located in v2rayN/ServiceLib/Manager/ClashApiManager.cs, this manager encapsulates every interaction with the Clash HTTP API, converting low‑level network calls into strongly‑typed .NET models that view‑models consume. For developers and power users, understanding how the ClashApiManager works for Clash‑compatible clients reveals the exact mechanism behind proxy switching, delay testing, and live connection management.

Architecture and Core Responsibilities

The manager operates as a singleton (ClashApiManager.Instance) and delegates all transport concerns to HttpClientHelper, which handles retries, timeouts, and JSON deserialization. It exposes six primary operational areas:

  • Proxy Discovery – Queries /proxies and /providers/proxies endpoints to build the node hierarchy.
  • Latency Testing – Parallel‑tests every node against /proxies/{name}/delay with configurable timeout and test URL.
  • Selector Control – Switches active nodes via PUT /proxies/{group} using header‑based payload.
  • Runtime Patching – Updates rule mode or log level through PATCH /configs.
  • Config Reloading – Hot‑reloads entire configuration files via PUT /configs?force=true.
  • Connection Inspection – Lists and terminates active connections through the /connections endpoint.

All methods construct the base URL dynamically using GetApiUrl(), which binds to the loopback interface and the port stored in AppManager.Instance.StatePort2.

Building the API URL

Before any request, the manager resolves the local Clash controller address. As implemented in ClashApiManager.cs (lines 183‑186):

private string GetApiUrl()
{
    return $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort2}";
}

This ensures that even if the core restarts on a different ephemeral port, subsequent calls target the correct endpoint without manual reconfiguration.

Fetching Proxies and Providers

The GetClashProxiesAsync() method retrieves both the flat proxy list and provider‑sourced nodes. It issues concurrent GET requests and deserializes the responses into ClashProxies and ClashProviders models (lines 18‑25):

var url   = $"{GetApiUrl()}/proxies";
var result = await HttpClientHelper.Instance.TryGetAsync(url);
var clashProxies = JsonUtils.Deserialize<ClashProxies>(result);

var url2   = $"{GetApiUrl()}/providers/proxies";
var result2 = await HttpClientHelper.Instance.TryGetAsync(url2);
var clashProviders = JsonUtils.Deserialize<ClashProviders>(result2);

The implementation retries failed requests three times with a two‑second backoff, caching the resulting dictionary in _proxies for downstream UI consumption.

Testing Proxy Latency

Node health checks run in parallel to prevent UI blocking. The ClashProxiesDelayTest() method (lines 67‑85) constructs a URL template pointing to the Clash delay endpoint, then dispatches tasks for every node in the supplied lstProxy list:

var urlBase = $"{GetApiUrl()}/proxies/{0}/delay?timeout=10000&url=" 
              + AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;

foreach (var it in lstProxy)
{
    var url = string.Format(urlBase, it.Name);
    tasks.Add(Task.Run(async () =>
    {
        var result = await HttpClientHelper.Instance.TryGetAsync(url);
        await updateFunc?.Invoke(it, result);
    }));
}
await Task.WhenAll(tasks);
await updateFunc?.Invoke(null, "");

The updateFunc callback—supplied by ClashProxiesViewModel—streams results back to the UI thread in real time, allowing live latency updates without freezing the interface.

Switching Active Proxy Nodes

When a user selects a new node within a selector group (e.g., switching from “Auto” to a specific server), ClashSetActiveProxy() executes a PUT request with a custom header (lines 13‑17):

var url = $"{GetApiUrl()}/proxies/{name}";
var headers = new Dictionary<string, string> { { "name", nameNode } };
await HttpClientHelper.Instance.PutAsync(url, headers);

If the Clash core returns HTTP 204, the view‑model updates its local selectedProxy.now property to reflect the change immediately, ensuring the UI stays synchronized with the core’s routing table.

Updating Runtime Configuration

For global settings like rule mode (Rule, Global, or Direct), the manager patches the running config via ClashConfigUpdate() (lines 31‑34):

var url = $"{GetApiUrl()}/configs";
await HttpClientHelper.Instance.PatchAsync(url, headers);

The headers dictionary contains key‑value pairs such as { "mode": "global" }, which Clash applies instantly without requiring a process restart.

Reloading Full Configuration Files

When users import a new profile, CoreConfigClashService invokes ClashConfigReload() to hot‑swap the entire configuration (lines 41‑45):

var url = $"{GetApiUrl()}/configs?force=true";
var headers = new Dictionary<string, string> { { "path", filePath } };
await HttpClientHelper.Instance.PutAsync(url, headers);

The force=true query parameter ensures Clash discards any in‑memory overrides and loads the file from disk exactly as specified.

How ViewModels Consume the Manager

ClashProxiesViewModel

Located in v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs, this view‑model orchestrates the proxy list UI:

  • Initialization – Calls ClashApiManager.Instance.GetClashProxiesAsync() to populate _proxies and _providers, then hydrates the observable collection bound to the frontend.
  • Delay Testing – Invokes ClashProxiesDelayTest() with blAll: true to benchmark every node, using RxUI’s MainThreadScheduler to marshal callbacks onto the UI thread.
  • Selector Switching – Executes ClashSetActiveProxy(selectedGroup.Name, selectedNode.Name) when the user clicks a new node, updating the backing field only after the HTTP call succeeds.

ClashConnectionsViewModel

This view‑model manages the live connections tab in ClashConnectionsViewModel.cs (lines 53‑62):

  • Polling – Periodically calls GetClashConnectionsAsync() to fetch active TCP/UDP flows from /connections.
  • Termination – Sends ClashConnectionClose(id) to issue a DELETE request against /connections/{id}, forcibly dropping the selected tunnel.

Practical Code Examples

Example 1 – Populating the Proxy List

public async Task LoadProxies()
{
    var ret = await ClashApiManager.Instance.GetClashProxiesAsync();
    if (ret?.Item1 != null)
    {
        _proxies = ret.Item1.proxies;
        await RefreshProxyGroups();
    }
}

Relevant source: ClashProxiesViewModel.cs (lines 64‑71).

Example 2 – Executing a Full Latency Test

await ClashApiManager.Instance.ClashProxiesDelayTest(
    blAll: true,
    lstProxy: ProxyDetails.ToList(),
    async (item, json) =>
    {
        var result = new SpeedTestResult { IndexId = item.Name, Delay = json };
        RxApp.MainThreadScheduler.Schedule(result, (scheduler, r) =>
        {
            _ = ProxiesDelayTestResult(r);
            return Disposable.Empty;
        });
    });

Relevant source: ClashProxiesViewModel.cs (lines 80‑88).

Example 3 – Changing the Active Node

public async Task ChangeProxy(string groupName, string nodeName)
{
    await ClashApiManager.Instance.ClashSetActiveProxy(groupName, nodeName);
    // Update local state only after successful API call
    selectedProxy.now = nodeName;
}

Relevant source: ClashProxiesViewModel.cs (lines 64‑66).

Summary

  • Centralized HTTP WrapperClashApiManager.cs abstracts all Clash REST endpoints, exposing async methods that return strongly‑typed ClashProxies and ClashProviders models.
  • Dynamic URL ResolutionGetApiUrl() builds the controller address from StatePort2, ensuring seamless reconnection after core restarts.
  • Parallel Latency TestingClashProxiesDelayTest() uses Task.WhenAll to test multiple nodes concurrently, streaming results via callbacks to prevent UI freezes.
  • State Synchronization – ViewModels such as ClashProxiesViewModel and ClashConnectionsViewModel consume the manager to keep the interface synchronized with the Clash core’s runtime state.

Frequently Asked Questions

Does ClashApiManager support Clash‑Meta or Mihomo cores?

Yes. Because the manager communicates through the standard Clash HTTP API specification, it is fully compatible with Clash‑Meta (now Mihomo) and other Clash‑compatible forks. The endpoint paths (/proxies, /configs, /connections) and request schemas remain consistent across these implementations, allowing v2rayN to control any core that exposes the standard REST interface.

How does the manager handle network failures or timeouts?

All HTTP operations route through HttpClientHelper.Instance, which implements a retry policy with exponential backoff. For proxy fetching, the code explicitly retries three times with a two‑second delay between attempts. If the Clash core is unreachable, the async method returns null or an empty result, and the calling view‑model logs the error without crashing the application.

Can I use ClashApiManager to modify DNS or TUN settings at runtime?

The manager exposes ClashConfigUpdate(), which sends a PATCH request to /configs. While the provided implementation focuses on rule mode switching, you can extend the headers dictionary to include any Clash runtime patch supported by your core (e.g., {"tun": {"enable": true}}), provided the core supports dynamic configuration updates for those specific fields.

Which file handles the actual reloading of a Clash configuration file?

CoreConfigClashService.cs invokes ClashApiManager.Instance.ClashConfigReload() when the user selects a new profile. This method performs a PUT request to /configs?force=true with the absolute file path in the headers, instructing the Clash core to reload the YAML configuration from disk immediately.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →