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:
- Creates the socket file or named pipe at the specified path.
- Spawns the mihomo binary as a child process, passing
--external-controllerpointing to the same IPC endpoint. - 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:
- App startup →
IClashTemp::guard_external_controller_ipc()generates the platform-specific socket path (e.g.,/.../verge-mihomo.sock). - Tauri builder registers
tauri-plugin-mihomowith that socket path via.socket_path(). - The plugin spawns
verge-mihomo(core) with--external-controllerpointing to the same IPC endpoint. - Frontend calls
getClashInfo()→ Tauriinvoke("get_clash_info"). - Rust command forwards a JSON-RPC request over the socket to the core, receives
ClashInfoJSON, and returns it to JavaScript. - UI opens a WebSocket via
MihomoWebSocket.connect_traffic(). - 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()insrc-tauri/src/config/clash.rsgenerates a stable socket path that is injected into both the Tauri plugin and the mihomo runtime configuration. - Dual interface: The
tauri-plugin-mihomoexposes both request-response commands (via Tauriinvoke) and streaming channels (via WebSocket over the same local socket) for real-time metrics. - Frontend abstraction: TypeScript services in
src/services/cmds.tsand React hooks insrc/hooks/use-mihomo-ws-subscription.tshide 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →