Monitoring

VPN monitoringUptime monitoringIntegrations

Platform

AI assistantsAPI and CLISecurity

More

PricingFree testerDocsBlog Log in

Guides

SIP003 explained: how Shadowsocks plugins work and how to test them

SIP003 is the plugin standard for Shadowsocks. A plugin is a separate program that the Shadowsocks client or server starts as a child process. It works as a local TCP tunnel and changes how the traffic looks on the wire, for example WebSocket over TLS. Shadowsocks tells it where to listen and where to connect with four environment variables (SS_REMOTE_HOST, SS_REMOTE_PORT, SS_LOCAL_HOST, SS_LOCAL_PORT) plus an optional SS_PLUGIN_OPTIONS string. In an ss:// link, the plugin and its options travel in the plugin= query parameter.

ST Muntaha11 min read

In my experience, most Shadowsocks plugin problems are not in the plugin's code. They are in the glue: a binary that is not on the service's PATH, a WebSocket path that differs by one character between server and client, or a client app whose built-in plugin does not support the mode the server runs. This post explains the SIP003 contract first, because once you know exactly what a plugin receives, those problems are quick to find. Then it covers the plugins people still run, the ss:// link format, the failure modes with their real log lines, and how to test a plugin setup from outside.

I ran every command below with shadowsocks-rust 1.24.0, v2ray-plugin 1.5.0 and sing-box 1.13.8. The current releases at the start of November 2026 are shadowsocks-rust 1.25.0, v2ray-plugin 1.5.0 and sing-box 1.14.4.

What is SIP003?

SIP003 is one of the Shadowsocks Improvement Proposals. The SIP003 document calls it "a simplified plugin design for shadowsocks". The idea comes from Tor's pluggable transports, with one simplification: every SIP003 plugin is a plain TCP tunnel (local port forwarding), not a SOCKS proxy. That keeps plugins small, because they never need to know where the traffic is going.

The data path from an app to the internet looks like this:

  1. The app sends traffic to the Shadowsocks client (sslocal, ss-local, or a phone app).
  2. The Shadowsocks client encrypts it as usual and sends it to the plugin client on a loopback port.
  3. The plugin client wraps it, for example in WebSocket over TLS, and sends it across the internet.
  4. The plugin server on the VPS unwraps it and passes it to the Shadowsocks server on a loopback port.
  5. The Shadowsocks server decrypts it and connects to the destination.

Shadowsocks encryption still runs end to end between the two Shadowsocks processes. The plugin changes only the outer shape of the connection. Even v2ray-plugin's plain WebSocket mode without TLS carries Shadowsocks-encrypted bytes; it just does not hide that they are WebSocket frames.

The spec sets three rules that matter when you operate it:

  • The plugin is a child process. The Shadowsocks process starts it. If the plugin exits with an error, the parent Shadowsocks process stops too. When you stop Shadowsocks, the plugin is terminated.
  • One plugin per server. The spec says "Plugin over plugin is NOT supported."
  • TCP only. SIP003 forwards TCP. A UDP extension (SIP003u) exists as an open proposal, and shadowsocks-rust implements it as the plugin_mode setting, but most plugins and clients are TCP only.

Which environment variables does a SIP003 plugin get?

A plugin gets four required variables and one optional one. Their meaning depends on which side the plugin runs on, and that is the part people get wrong.

Variable On the client On the server
SS_LOCAL_HOST, SS_LOCAL_PORT Loopback address where the plugin listens for the Shadowsocks client Loopback address where the Shadowsocks server now listens
SS_REMOTE_HOST, SS_REMOTE_PORT The VPS address and port to connect to The public address and port the plugin must listen on
SS_PLUGIN_OPTIONS The client's plugin options The server's plugin options

To see this for real, I wrote a fake plugin that only prints its environment, and pointed ssserver and sslocal at it. On the server, with "server": "0.0.0.0" and "server_port": 18500:

SS_LOCAL_HOST=127.0.0.1
SS_LOCAL_PORT=51766
SS_PLUGIN_OPTIONS=server;host=vpn.yourdomain.com;path=/tunnel
SS_REMOTE_HOST=0.0.0.0
SS_REMOTE_PORT=18500

And on the client, connecting to vpn.yourdomain.com:18500:

SS_LOCAL_HOST=127.0.0.1
SS_LOCAL_PORT=51777
SS_PLUGIN_OPTIONS=host=vpn.yourdomain.com;path=/tunnel
SS_REMOTE_HOST=vpn.yourdomain.com
SS_REMOTE_PORT=18500

The local port is random: shadowsocks-rust asks the OS for a free loopback port each time it starts. The important consequence is on the server. With a plugin, the plugin owns your public port, and ssserver moves to loopback. shadowsocks-rust says so in its startup log:

INFO shadowsocks tcp server listening on 127.0.0.1:51766, inbound address 0.0.0.0:18500

So if you check ss -ltnp 'sport = :443' on a plugin server, the process on port 443 should be the plugin (v2ray-plugin, shadow-tls), not ssserver. It also means the plugin needs permission to bind a low port. Under systemd, AmbientCapabilities=CAP_NET_BIND_SERVICE on the Shadowsocks unit is inherited by the plugin it starts.

SS_PLUGIN_OPTIONS is a key=value list separated by semicolons. A bare key is a flag, such as server or tls. Semicolons, equals signs and backslashes inside a value must be escaped with a backslash, so a WebSocket path of /ws;v2 is written path=/ws\;v2.

The plugin goes in the plugin query parameter defined by SIP002. Its value is the plugin name, a semicolon, then the options, all URL-encoded:

ss://userinfo@hostname:port/?plugin=url-encoded-plugin-and-options#tag

Three details from the spec cause most broken links:

  • Put a / before the ? when a plugin is present.
  • For the classic AEAD methods (aes-256-gcm, chacha20-ietf-poly1305), userinfo is usually base64url(method:password). For Shadowsocks 2022 methods, the spec says userinfo MUST NOT be base64 encoded: write method:password with both parts percent-encoded.
  • Clients must ignore parameters they do not understand, so a typo in a parameter name fails silently.

A Shadowsocks 2022 link with v2ray-plugin over TLS:

ss://2022-blake3-aes-128-gcm:SltovJBRjzfjux%2FH%2BsIZdA%3D%[email protected]:443/?plugin=v2ray-plugin%3Btls%3Bhost%3Dvpn.yourdomain.com%3Bpath%3D%2Fws-k7Qe2#ss-ws-tls

A classic AEAD link with simple-obfs:

ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpUcTlWYjJ4TG00UmZaOHNX@vpn.yourdomain.com:8388/?plugin=obfs-local%3Bobfs%3Dhttp%3Bobfs-host%3Dwww.yourdomain.com#ss-obfs

shadowsocks-rust ships ssurl, which decodes a link back into a config, so you can check what a client will see. ssurl -e config.json does the reverse:

$ ssurl -d 'ss://2022-blake3-aes-128-gcm:SltovJBRjzfjux%2FH%2BsIZdA%3D%[email protected]:443/?plugin=v2ray-plugin%3Btls%3Bhost%3Dvpn.yourdomain.com%3Bpath%3D%2Fws-k7Qe2#ss-ws-tls'
      password: "SltovJBRjzfjux/H+sIZdA==",
      method: "2022-blake3-aes-128-gcm",
      plugin: "v2ray-plugin",
      plugin_opts: "tls;host=vpn.yourdomain.com;path=/ws-k7Qe2",
      remarks: "ss-ws-tls",

Which SIP003 plugins are still maintained?

v2ray-plugin is the one I would start with today. It is maintained under the shadowsocks organisation and had a release in October 2026. The others either stopped or serve a narrower purpose.

Plugin What an observer sees Outer transport Latest release I found
simple-obfs (obfs-local, obfs-server) A fake HTTP request or fake TLS handshake at the start of the connection TCP v0.0.5, November 2017. Its README says "Deprecated. Followed by v2ray-plugin."
v2ray-plugin WebSocket (optionally with TLS), QUIC, or a Hysteria2 transport TCP, or UDP for QUIC and Hysteria2 v1.5.0, October 2026
xray-plugin WebSocket, TLS, QUIC, gRPC TCP or UDP v1.8.24, February 2026
shadow-tls A real TLS handshake with a site you choose, using that site's certificate TCP v0.2.25, December 2023
Cloak Traffic shaped like ordinary web browsing; non-Cloak connections are passed to a real site TCP v2.12.0, July 2025

Some notes on each, from their docs and source:

simple-obfs wraps only the start of the connection to look like HTTP (obfs=http) or TLS (obfs=tls), with a fake host name in obfs-host. There is no real TLS and no certificate. It is deprecated, but you will still meet it, because sing-box and Mihomo include an obfs client built in. sing-box calls it obfs-local; Mihomo calls it obfs and takes mode and host instead of obfs and obfs-host.

v2ray-plugin carries the Shadowsocks stream over WebSocket by default. tls adds real TLS, mode=quic switches to QUIC, and version 1.5.0 added mode=hysteria2, a transport-only mode that does not work with ordinary Hysteria2 proxy servers. Its command-line reference lists every option. Three defaults surprise people: host defaults to cloudfront.com, path defaults to /, and boolean options work by presence, so tls=false still turns TLS on.

shadow-tls relays the TLS handshake to a real TLS server (yours or a large public site), so an observer sees a genuine certificate. In SIP003 mode it reads server, tls=host:443 (server side), host=... (client side), passwd=... and v3 from the options string. Version 3 in strict mode needs a handshake server with TLS 1.3, which you can check with openssl s_client -tls1_3 -connect www.yourdomain.com:443. It is built on io_uring and does not support Windows.

Cloak works as a SIP003 plugin or standalone, supports many users on one port, and sends connections that are not from a Cloak client to RedirAddr, a real website. Its config is a JSON file rather than a few options.

How do I set up v2ray-plugin with WebSocket and TLS?

Install the plugin binary on both machines, then point the Shadowsocks config at it. Server config for shadowsocks-rust (ssserver -c /etc/shadowsocks-rust/config.json):

{
    "server": "0.0.0.0",
    "server_port": 443,
    "method": "2022-blake3-aes-128-gcm",
    "password": "SltovJBRjzfjux/H+sIZdA==",
    "mode": "tcp_only",
    "plugin": "v2ray-plugin",
    "plugin_opts": "server;tls;host=vpn.yourdomain.com;path=/ws-k7Qe2;cert=/etc/ssl/vpn/fullchain.pem;key=/etc/ssl/vpn/privkey.pem"
}

Client config for sslocal:

{
    "server": "vpn.yourdomain.com",
    "server_port": 443,
    "method": "2022-blake3-aes-128-gcm",
    "password": "SltovJBRjzfjux/H+sIZdA==",
    "local_address": "127.0.0.1",
    "local_port": 1080,
    "plugin": "v2ray-plugin",
    "plugin_opts": "tls;host=vpn.yourdomain.com;path=/ws-k7Qe2"
}

Generate your own key with openssl rand -base64 16; do not copy the one above. If you leave out cert and key, the plugin looks for an acme.sh certificate in ~/.acme.sh/vpn.yourdomain.com/ and then in ~/.acme.sh/vpn.yourdomain.com_ecc/. The host and path must match exactly on both sides.

Why "mode": "tcp_only"? Because SIP003 carries TCP only. shadowsocks-rust serves Shadowsocks UDP itself, directly on the public port, when the plugin mode does not include UDP. With tcp_and_udp and a TCP plugin, your UDP relay runs as plain Shadowsocks next to the WebSocket port. That may be what you want, but it should be a decision, not an accident. With mode=quic or mode=hysteria2 the plugin itself listens on UDP, so open UDP on that port in the firewall and keep ssserver on tcp_only.

If your users run sing-box, they do not need the binary: sing-box implements obfs-local and v2ray-plugin internally. The outbound looks like this:

{
  "type": "shadowsocks",
  "server": "vpn.yourdomain.com",
  "server_port": 443,
  "method": "2022-blake3-aes-128-gcm",
  "password": "SltovJBRjzfjux/H+sIZdA==",
  "plugin": "v2ray-plugin",
  "plugin_opts": "tls;host=vpn.yourdomain.com;path=/ws-k7Qe2"
}

What breaks in a SIP003 setup?

These are the failures I check for, in the order I check them. The log lines are from shadowsocks-rust 1.24.0 and sing-box 1.13.8.

1. The plugin binary is not found. Common under systemd, where PATH is shorter than in your shell. ssserver logs this and exits:

ERROR failed to start plugin "v2ray-plugin" for server 0.0.0.0:443, err: No such file or directory (os error 2)
server aborted with No such file or directory (os error 2)

Fix: use the absolute path in "plugin", for example /usr/local/bin/v2ray-plugin.

2. The plugin starts and then exits. A wrong option, a missing certificate file or a port already in use makes the plugin quit. Because of the SIP003 lifecycle, ssserver stops with it:

ERROR plugin exited with status: exit status: 1
server aborted with server exited unexpectedly

The plugin's own stderr usually appears just above these lines in the journal, so read upwards.

3. The options do not match between server and client. A different path, host, mode, or tls on one side only. This is the quiet one. In my tests with a wrong path, neither ssserver, sslocal nor v2ray-plugin logged anything at the default level that pointed to the cause. The client's SOCKS port accepted connections, and every request through it failed. In the lab, curl reported SSL_ERROR_SYSCALL. A port check stays green the whole time, the same trap as with WireGuard port monitoring.

4. The client's built-in plugin does not support the server's mode. sing-box and Mihomo implement plugins themselves, and only some modes. sing-box supports v2ray-plugin's websocket and quic modes, and refuses anything else at startup:

FATAL initialize outbound[0]: v2ray-plugin: unknown mode: hysteria2
FATAL create service: initialize outbound[0]: plugin not found: xray-plugin

Mihomo's docs mark its v2ray-plugin as WebSocket only ("no QUIC now"). Before you move a server to a new mode, check what your users' apps can speak.

5. The certificate expires. TLS modes use a normal certificate, and it expires like any other. Clients that verify certificates stop connecting on the day it expires. Check the date from outside (see the next section) and make sure your renewal job actually runs.

How do I test a Shadowsocks plugin setup from outside?

Test from the outside in: the listener, then the outer transport, then a real request through the whole chain. Each step tells you which layer is broken.

Step 1: the listener. On the server, the public port should belong to the plugin:

ss -ltnp 'sport = :443'

Step 2: the certificate (TLS modes only), from another machine:

echo | openssl s_client -connect vpn.yourdomain.com:443 -servername vpn.yourdomain.com 2>/dev/null | openssl x509 -noout -subject -enddate

Step 3: the WebSocket handshake. v2ray-plugin answers a WebSocket upgrade on the right path and refuses others, so curl can test the path without any Shadowsocks client:

curl -s -m 5 -i --http1.1 \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://vpn.yourdomain.com/ws-k7Qe2 | head -n 1

What I got from v2ray-plugin 1.5.0:

  • Right path: HTTP/1.1 101 Switching Protocols
  • Wrong path: HTTP/1.1 404 Not Found
  • Right path, but a normal GET without the upgrade headers: HTTP/1.1 400 Bad Request

Curl waits after a 101, which is why the command has -m 5.

Step 4: a real request through the whole chain. Run a client from a machine outside the server's network, with the same link your users get, and fetch a page that returns a known status:

sslocal -b 127.0.0.1:1080 --server-url 'ss://2022-blake3-aes-128-gcm:SltovJBRjzfjux%2FH%2BsIZdA%3D%[email protected]:443/?plugin=v2ray-plugin%3Btls%3Bhost%3Dvpn.yourdomain.com%3Bpath%3D%2Fws-k7Qe2#ss-ws-tls' &
curl -s -m 10 --socks5-hostname 127.0.0.1:1080 -o /dev/null -w '%{http_code}\n' https://www.gstatic.com/generate_204

The answer should be 204. sslocal starts the external plugin binary, so v2ray-plugin must be installed on the test machine too. Only this step proves the key, the cipher, the plugin options and the server's outbound all work together. Steps 1 to 3 only tell you where to look when step 4 fails.

Step 4 is also the only one worth running on a schedule. A single test from your laptop proves the server worked once, from one network. The VPN server monitoring guide covers how often to check and from where.

Full disclosure: TunnelHQ runs this kind of check for you. It connects to a Shadowsocks server from outside with the real client flow from the ss:// link and sends a request through the tunnel every 1 to 5 minutes, depending on plan. Shadowsocks monitoring explains the check, and every plan starts with a 14-day free trial. To test one link once, paste it into the free tester, no account needed.