How IPC Communication Between Multiple Shadowsocks Instances Works

Shadowsocks-Windows uses a named mutex to enforce a single UI instance and delegates commands from secondary processes to the primary instance through a lightweight named-pipe IPC protocol.

Shadowsocks-Windows is architected to prevent multiple UI windows from running simultaneously while still supporting command-line interactions. When you launch a second copy of the application, it detects the existing instance and routes operations through a IPC communication between multiple Shadowsocks instances mechanism based on Windows named pipes and a custom binary protocol.

Single-Instance Detection via Named Mutex

The application implements a robust singleton pattern using a system-wide mutex to ensure only one UI process manages the proxy configuration.

Mutex Creation and Naming Convention

At startup, the application attempts to create a named mutex with a unique identifier derived from the executable path. In shadowsocks-csharp/Program.cs, the mutex is defined as:

new Mutex(true, $"Shadowsocks_{ExecutablePath.GetHashCode()}")

This naming scheme ensures that different builds or copies of the executable running from separate directories operate independently, while identical paths share the same mutex namespace.

Detecting Existing Instances

The startup logic checks mutex ownership to determine if another instance is active. From shadowsocks-csharp/Program.cs (lines 46-49):

bool hasAnotherInstance = !mutex.WaitOne(TimeSpan.Zero, true);

If hasAnotherInstance evaluates to true, the current process recognizes it is secondary and must communicate with the primary instance rather than launching its own UI.

Named Pipe IPC Protocol Architecture

The IPC system relies on Windows named pipes with a uniquely generated pipe name that matches both instances.

Unique Pipe Identification

Both primary and secondary instances derive the same pipe identifier from the executable path hash. In shadowsocks-csharp/Controller/Service/IPCService.cs (line 22), the pipe name follows this pattern:


Shadowsocks\<ExecutablePathHash>

This ensures that multiple Shadowsocks installations on the same machine do not interfere with each other's IPC channels.

Message Structure

The protocol uses a minimal binary format consisting of:

  1. A 4-byte opcode (integer)
  2. A 4-byte length prefix (integer)
  3. The UTF-8 payload (variable length)

Currently, the only supported operation is OP_OPEN_URL = 1, which instructs the primary instance to parse and add a Shadowsocks server URL.

Server-Side Implementation in the Primary Instance

The primary instance spawns an IPCService object that listens for incoming connections on a background Task.

The IPCService Listening Loop

The server implementation in shadowsocks-csharp/Controller/Service/IPCService.cs (lines 26-48) creates a NamedPipeServerStream in an infinite loop:

public async Task RunServer()
{
    while (true)
    {
        using (var pipe = new NamedPipeServerStream(pipeName, ...))
        {
            await pipe.WaitForConnectionAsync();
            int opcode = pipe.ReadByte(); // Simplified; actual uses 4-byte read
            if (opcode == OP_OPEN_URL)
            {
                // Read length and URL bytes
                byte[] urlBytes = ...;
                string url = Encoding.UTF8.GetString(urlBytes);
                OpenUrlRequested?.Invoke(this, new RequestAddUrlEventArgs(url));
            }
        }
    }
}

This asynchronous pattern ensures the UI remains responsive while handling IPC requests.

Processing Open URL Requests

When the server receives a valid OP_OPEN_URL command, it raises the OpenUrlRequested event. The main application subscribes to this event in shadowsocks-csharp/Program.cs:

ipcService.OpenUrlRequested += (_, e) => 
    MainController.AskAddServerBySSURL(e.Url);

The AskAddServerBySSURL method in shadowsocks-csharp/Controller/ShadowsocksController.cs (lines 421-432) parses the ss:// link and adds the server configuration to the running instance.

Client-Side Implementation for Secondary Instances

When a secondary instance detects an existing primary process, it acts as an IPC client rather than initializing the full application stack.

RequestOpenUrl Method

The client implementation in shadowsocks-csharp/Controller/Service/IPCService.cs (lines 74-86) connects to the named pipe with a 10-millisecond timeout:

public static void RequestOpenUrl(string url)
{
    using (var pipe = new NamedPipeClientStream(".", pipeName, ...))
    {
        pipe.Connect(10);
        byte[] urlBytes = Encoding.UTF8.GetBytes(url);
        pipe.Write(BitConverter.GetBytes(OP_OPEN_URL), 0, 4);
        pipe.Write(BitConverter.GetBytes(urlBytes.Length), 0, 4);
        pipe.Write(urlBytes, 0, urlBytes.Length);
    }
}

After writing the opcode, length, and payload, the client closes the pipe and terminates immediately without displaying any UI.

Command-Line Trigger (--openurl)

Secondary instances are typically launched via command-line arguments. To add a server from the command line:

Shadowsocks.exe --openurl "ss://aes-256-cfb:password@host:port#My%20Server"

If the secondary instance is started without the --openurl argument, it simply displays a message box directing the user to the existing tray icon, as implemented in shadowsocks-csharp/Program.cs (lines 61-66).

Summary

  • Mutex-based singleton: Shadowsocks-Windows creates a named mutex (Shadowsocks_{hash}) to prevent multiple UI instances.
  • Named pipe identification: IPC channels use the identifier Shadowsocks\<ExecutablePathHash> to ensure uniqueness per installation.
  • Binary protocol: Communication uses a 4-byte opcode followed by length-prefixed UTF-8 strings.
  • Current capability: The system exclusively supports the OP_OPEN_URL operation for adding servers via ss:// links.
  • Graceful delegation: Secondary processes exit immediately after relaying commands, leaving the primary instance to handle all state changes.

Frequently Asked Questions

How does Shadowsocks-Windows prevent multiple UI windows from opening?

The application creates a system-wide named mutex at startup in shadowsocks-csharp/Program.cs. If mutex.WaitOne() returns false, indicating another process already holds the mutex, the new instance recognizes it is secondary and either sends an IPC command or displays a notification, then exits without creating a window.

What protocol does the IPC system use to communicate?

The IPC mechanism uses Windows named pipes with a custom binary protocol. Messages consist of a 4-byte integer opcode (currently only OP_OPEN_URL = 1 is supported), followed by a 4-byte length field, and the UTF-8 encoded payload. This implementation resides in shadowsocks-csharp/Controller/Service/IPCService.cs.

Can I programmatically send commands to a running Shadowsocks instance?

Yes, any process can invoke the static IPCService.RequestOpenUrl() method after connecting to the named pipe Shadowsocks\<ExecutablePathHash>. This allows scripts or browser extensions to add servers by sending properly formatted ss:// URLs to the primary instance without restarting the application.

Where is the IPC logic implemented in the source code?

The three critical files are:

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 →