IPC Communication Architecture Between Clash Verge Rev Frontend and mihomo Core

The IPC communication architecture between Clash Verge Rev frontend and mihomo core relies on a local-socket channel provided by the tauri-plugin-mihomo plugin, enabling secure JSON-RPC commands and WebSocket streaming without exposing network ports.

Clash Verge Rev is a Tauri-based desktop proxy client that wraps the mihomo core engine. To maintain security and eliminate port conflicts, the application uses a platform-native IPC mechanism—Unix domain sockets on Linux/macOS and named pipes on Windows—to bridge the React/TypeScript frontend with the Rust backend and mihomo process.

Architecture Overview

The IPC stack is organized into three distinct layers that handle path generation, socket management, and API consumption:

Layer Responsibility Key Implementation
Configuration Generates a consistent IPC endpoint (external-controller-ipc) and injects it into the Clash configuration. src-tauri/src/config/clash.rs – guard_external_controller_ipc()
Tauri Plugin Starts a local-socket server that forwards JSON-RPC calls to the mihomo binary. src-tauri/src/lib.rs – plugin builder with .socket_path(...)
Frontend API Wraps Tauri commands (invoke) and WebSocket connections for real-time data. src/services/cmds.ts, src/hooks/use-mihomo-ws-subscription.ts

Generating the IPC Endpoint

Path Resolution

When the application launches, IClashTemp::guard_external_controller_ipc() in src-tauri/src/config/clash.rs resolves a stable filesystem path for the socket:

// src-tauri/src/config/clash.rs
pub fn guard_external_controller_ipc() -> String {
    // Always use the current IPC path, ensuring config & runtime path match
    ipc_path()
        .ok()
        .and_then(|path| path_to_str(&path).ok().map(|s| s.into()))
        .unwrap_or_else(|| {
            logging!(error, Type::Config, "Failed to get IPC path");
            crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
        })
}

The helper ipc_path() (located in src-tauri/src/utils/dirs.rs) returns a platform-specific location:

  • Linux/macOS: ~/.local/share/clash-verge-rev/ipc/verge-mihomo.sock
  • Windows: \\.\pipe\verge-mihomo

Configuration Injection

The resolved path is injected into the generated Clash configuration template to ensure the core listens on the same endpoint:

// src-tauri/src/config/clash.rs (within template())
map.insert("external-controller-unix".into(), Self::guard_external_controller_ipc().into());
// Windows uses "external-controller-pipe"

This guarantees that both the mihomo core and the Tauri plugin reference the identical IPC endpoint.

Tauri Plugin Registration

Socket Initialization

The main Tauri entry point registers the tauri-plugin-mihomo plugin with the computed socket path:

// src-tauri/src/lib.rs
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
    let mut builder = builder
        .plugin(tauri_plugin_mihomo::Builder::new()
            .protocol(tauri_plugin_mihomo::models::Protocol::LocalSocket) // use local socket
            .socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
            .build());
    // …other plugins
    builder
}

The plugin performs three critical tasks:

  1. Creates the socket file or named pipe at the specified path.
  2. Spawns the mihomo binary as a child process, passing --external-controller pointing to the same IPC endpoint.
  3. Exposes Tauri commands that forward JSON-RPC requests to the core over the socket.

Command Registration

The Tauri application registers specific commands that act as the RPC bridge:

// src-tauri/src/lib.rs (excerpt)
tauri::generate_handler![
    // …system commands
    cmd::get_clash_info,
    cmd::patch_clash_config,
    // …profile, runtime, etc.
];

These commands are implemented in src-tauri/src/cmd/clash.rs and other modules within the cmd directory.

Frontend Integration

One-off Commands via Invoke

The frontend uses thin TypeScript wrappers around Tauri's invoke function to call Rust commands synchronously:

// src/services/cmds.ts (excerpt)
import { invoke } from "@tauri-apps/api/core";

export const getClashInfo = () => invoke<IClashInfo | null>("get_clash_info");
export const patchClashConfig = (payload: Partial<IClashConfig>) =>
  invoke<void>("patch_clash_config", { payload });

When getClashInfo() is called, the Tauri runtime marshals the request to JSON, transmits it through the local socket to the mihomo process, retrieves the response, and resolves the JavaScript Promise with the structured data.

Real-time Streams via WebSocket

For high-frequency data such as traffic statistics, logs, and active connections, the plugin exposes a WebSocket server bound to the same IPC socket. The frontend consumes these streams through dedicated React hooks.

The core subscription logic resides in src/hooks/use-mihomo-ws-subscription.ts:

// src/hooks/use-mihomo-ws-subscription.ts (excerpt)
import { type Message, type MihomoWebSocket } from "tauri-plugin-mihomo-api";

export const useMihomoWsSubscription = <T>(
  subscribe: (ws: MihomoWebSocket) => Promise<void> | void,
) => {
  const wsRef = useRef<MihomoWebSocket | null>(null);
  // …connect, event handling, cleanup
};

Concrete implementations create specific connections, such as traffic monitoring in src/hooks/use-traffic-data.ts:

// src/hooks/use-traffic-data.ts
import { MihomoWebSocket, Traffic } from "tauri-plugin-mihomo-api";

export const useTrafficData = () => useMihomoWsSubscription<Traffic>({
  connect: () => MihomoWebSocket.connect_traffic(),
});

The WebSocket protocol, implemented in the plugin's Rust layer (tauri-plugin-mihomo/src/ws.rs), pushes Message objects (e.g., {type: "traffic", data: …}) that React hooks consume to update the UI instantly.

End-to-End Communication Flow

The complete IPC lifecycle follows these steps:

  1. App startup → IClashTemp::guard_external_controller_ipc() generates the platform-specific socket path (e.g., /.../verge-mihomo.sock).
  2. Tauri builder registers tauri-plugin-mihomo with that socket path via .socket_path().
  3. The plugin spawns verge-mihomo (core) with --external-controller pointing to the same IPC endpoint.
  4. Frontend calls getClashInfo() → Tauri invoke("get_clash_info").
  5. Rust command forwards a JSON-RPC request over the socket to the core, receives ClashInfo JSON, and returns it to JavaScript.
  6. UI opens a WebSocket via MihomoWebSocket.connect_traffic().
  7. Core pushes traffic updates through the WebSocket → UI hook updates the traffic chart.

All communication remains local to the host process, avoiding external network exposure and respecting OS-specific sandboxing rules.

Code Examples

Retrieve Core Information (One-off Request)

import { getClashInfo } from "@/services/cmds";

async function showCoreInfo() {
  const info = await getClashInfo();
  console.log("Mihomo is listening on", info?.server);
  console.log("Mixed port:", info?.mixed_port);
}
showCoreInfo();

Behind the scenes: invoke("get_clash_info") triggers cmd::get_clash_info in src-tauri/src/cmd/clash.rs, which performs JSON-RPC over the IPC socket and returns a ClashInfo struct.

Subscribe to Real-Time Traffic

import { useTrafficData } from "@/hooks/use-traffic-data";

export function TrafficChart() {
  const { data, error } = useTrafficData();

  if (error) return <div>Error loading traffic</div>;
  if (!data) return <div>Loading…</div>;

  return (
    <div>
      Upload: {data.upload} B • Download: {data.download} B
    </div>
  );
}

Behind the scenes: useTrafficData creates a MihomoWebSocket via connect_traffic(), opening a WebSocket on the local socket and listening for Message::Traffic events pushed by the core.

Patch the Clash Config at Runtime

import { patchClashConfig } from "@/services/cmds";

async function setAllowLan(allow: boolean) {
  await patchClashConfig({ allow_lan: allow });
  // The core reloads the config automatically via the plugin.
}

Behind the scenes: invoke("patch_clash_config", { payload }) routes to cmd::patch_clash_config, which writes a temporary YAML and signals the mihomo instance to reload via its IPC command interface.

Key Implementation Files

File Role Link
src-tauri/src/lib.rs Registers tauri-plugin-mihomo and supplies the socket path. src-tauri/src/lib.rs
src-tauri/src/config/clash.rs Generates Clash config, defines guard_external_controller_ipc(). src-tauri/src/config/clash.rs
src-tauri/src/utils/dirs.rs Resolves the IPC file location (ipc_path). src-tauri/src/utils/dirs.rs
src-tauri/src/cmd/*.rs (e.g., clash.rs) Implements the Tauri commands that forward RPC to mihomo. src-tauri/src/cmd/clash.rs
src/services/cmds.ts Thin TypeScript wrappers around invoke. src/services/cmds.ts
src/hooks/use-mihomo-ws-subscription.ts Core WebSocket subscription logic for the UI. src/hooks/use-mihomo-ws-subscription.ts
src/hooks/use-traffic-data.ts Example consumer of the traffic WebSocket. src/hooks/use-traffic-data.ts
src-tauri/src/feat/* Feature modules (proxy, profile, runtime) that invoke the commands. src-tauri/src/feat

Summary

  • Local-socket IPC: The architecture uses platform-native sockets (Unix domain sockets on Linux/macOS, named pipes on Windows) to eliminate network exposure and sandbox the mihomo core.
  • Path consistency: guard_external_controller_ipc() in src-tauri/src/config/clash.rs generates a stable socket path that is injected into both the Tauri plugin and the mihomo runtime configuration.
  • Dual interface: The tauri-plugin-mihomo exposes both request-response commands (via Tauri invoke) and streaming channels (via WebSocket over the same local socket) for real-time metrics.
  • Frontend abstraction: TypeScript services in src/services/cmds.ts and React hooks in src/hooks/use-mihomo-ws-subscription.ts hide the IPC complexity, exposing typed async functions and reactive data streams.

Frequently Asked Questions

How does Clash Verge Rev prevent port conflicts with other applications?

By using local sockets instead of TCP ports, the IPC communication architecture avoids binding to any network interface. The socket path is generated in src-tauri/src/utils/dirs.rs and stored in the application data directory, ensuring it is unique to the Clash Verge Rev instance and does not collide with other software.

What is the difference between Tauri invoke and the MihomoWebSocket?

Tauri invoke is used for stateless, request-response operations such as retrieving configuration or updating settings. It maps to Rust commands in src-tauri/src/cmd/*.rs that perform single JSON-RPC transactions over the socket. MihomoWebSocket is used for persistent, bidirectional streaming of high-frequency data like traffic statistics, logs, and connection lists, maintaining an open WebSocket connection over the same local socket.

Can the IPC mechanism work across network boundaries?

No. The architecture is explicitly designed for local-only communication. The socket path resolves to a filesystem location on the host (Unix domain socket) or a local named pipe (Windows). This design prevents remote exploitation of the mihomo control API and respects OS sandboxing policies, keeping all control plane traffic internal to the machine.

How does the frontend handle reconnection if the mihomo core restarts?

The React hooks in src/hooks/use-mihomo-ws-subscription.ts implement connection lifecycle management. When the WebSocket connection drops (indicating a core restart or crash), the hook detects the closure event and attempts to re-establish the connection to the same socket path generated by guard_external_controller_ipc(). Meanwhile, one-off invoke calls will fail gracefully until the core is back online, allowing the UI to display appropriate error states.

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 →