Skip to content

Shadowsocks

Shadowsocks is an encrypted proxy protocol with no handshake of its own: the client’s first bytes on the wire are a random salt, and everything after the salt is encrypted, the destination address included. etemenanki-app speaks it as a server ([[inbound]]) and as a client ([[outbound]]), in two families:

  • Legacy AEAD (SIP004), where every user derives a key from a password. Use it for older clients that do not support 2022.
  • Shadowsocks 2022 (SIP022), which uses random base64 keys, BLAKE3 key derivation and a timestamp in every request. Prefer it for new deployments.

The method value picks the family, and both families support several users on one port. TCP only Neither side carries UDP.

Legacy AEAD Shadowsocks 2022
method values aes-128-gcm, aes-256-gcm, chacha20-poly1305, xchacha20-poly1305, and aliases 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm, 2022-blake3-chacha20-poly1305
Method name matching Case-insensitive Exact
password holds Any string; the key is derived from it A base64 key of the cipher’s key length (16 or 32 bytes)
Several users on one port Yes, with any method Yes, with the two AES-GCM methods only
How the server finds the user Tries each user’s key in turn Reads an encrypted identity header
Clocks must agree No Yes, within 30 seconds
Stream ciphers, none, plain Not supported Not supported

This sets up a Shadowsocks 2022 server with two users, and an etemenanki-app client for one of them.

  1. Generate one identity key for the server and one key per user. 2022-blake3-aes-256-gcm takes 32-byte keys:

    Terminal window
    openssl rand -base64 32 # the server's identity PSK (iPSK)
    openssl rand -base64 32 # alice's key (uPSK)
    openssl rand -base64 32 # bob's key (uPSK)
  2. Put the keys into the server config and check it:

    Terminal window
    etemenanki-app --test -c /etc/etemenanki/config.toml
  3. Give each user the password <iPSK>:<uPSK>, their own key after the server’s identity key, together with the method, address and port.

/etc/etemenanki/config.toml
# A Shadowsocks 2022 server on port 8388 with two users and a direct exit.
# Every key here is a placeholder. Generate each one with
# `openssl rand -base64 32` (2022-blake3-aes-256-gcm takes 32-byte keys).
[log]
level = "info"
[[inbound]]
tag = "ss-in"
protocol = "shadowsocks"
listen = "0.0.0.0"
port = 8388
[inbound.settings]
method = "2022-blake3-aes-256-gcm"
# The identity PSK (iPSK). Every client puts it first in its password.
password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
# One entry per user. Each `password` is that user's own key (uPSK), so a
# client's full password is "<iPSK>:<uPSK>".
[[inbound.settings.users]]
password = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE="
email = "alice@example.com"
[[inbound.settings.users]]
password = "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII="
email = "bob@example.com"
[[outbound]]
tag = "direct"
protocol = "freedom"
[[outbound]]
tag = "block"
protocol = "blackhole"
[route]
default = "direct"
# Keep clients away from the server's own private networks.
[[route.rule]]
outbound = "block"
cidr = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "fc00::/7", "::1/128"]

Other Shadowsocks 2022 clients take the same values: method 2022-blake3-aes-256-gcm and password <iPSK>:<uPSK>. Turn off UDP relay in those clients; see TCP only.

Every accepted method value, for both the inbound and the outbound:

method Aliases Family Key source Key and salt length Several users
aes-128-gcm aead_aes_128_gcm Legacy Password 16 bytes Yes
aes-256-gcm aead_aes_256_gcm Legacy Password 32 bytes Yes
chacha20-poly1305 chacha20-ietf-poly1305, aead_chacha20_poly1305 Legacy Password 32 bytes Yes
xchacha20-poly1305 xchacha20-ietf-poly1305 Legacy Password 32 bytes Yes
2022-blake3-aes-128-gcm none 2022 Base64 PSK 16 bytes Yes
2022-blake3-aes-256-gcm none 2022 Base64 PSK 32 bytes Yes
2022-blake3-chacha20-poly1305 none 2022 Base64 PSK 32 bytes No

How etemenanki-app reads the value:

  • A value that starts with 2022- is a 2022 method and must match one of the three names exactly. 2022-BLAKE3-AES-128-GCM fails with unknown shadowsocks-2022 method.
  • Any other value is a legacy method, matched without regard to case, so AES-256-GCM and CHACHA20-IETF-POLY1305 work.
  • Neither kind is trimmed. " aes-128-gcm" with a leading space fails.
  • There are no stream ciphers (aes-256-cfb, rc4-md5 and the like), and no none or plain method. They fail with unknown shadowsocks method.

Legacy methods derive a master key from the password with OpenSSL’s EVP_BytesToKey, then a per-connection subkey with HKDF-SHA1 and a random salt. Shadowsocks 2022 methods derive the per-connection subkey from the PSK and a random salt with BLAKE3.

For Shadowsocks 2022, every key (the single PSK, the iPSK and each uPSK) is random bytes of the cipher’s key length, written in standard base64 with padding:

method Command Output length
2022-blake3-aes-128-gcm openssl rand -base64 16 24 characters, ending in ==
2022-blake3-aes-256-gcm, 2022-blake3-chacha20-poly1305 openssl rand -base64 32 44 characters, ending in =

The URL-safe alphabet (- and _) and unpadded keys are rejected. Spaces or a newline around the key are trimmed.

For a legacy method, the password can be any string. Use a long random one, for example the output of openssl rand -base64 24.

protocol = "shadowsocks" in an [[inbound]], with these keys under [inbound.settings]. The common inbound keys (tag, listen, port, sniffing) are described on Inbounds.

KeyTypeRequiredDefaultDescription
methodstring (enum)yes—The cipher. A value that starts with 2022- selects Shadowsocks 2022 and must be exactly 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm or 2022-blake3-chacha20-poly1305. Anything else is a legacy AEAD method, matched case-insensitively: aes-128-gcm, aes-256-gcm, chacha20-poly1305, xchacha20-poly1305, or one of their aliases. The value is not trimmed. An unknown value fails with unknown shadowsocks method or unknown shadowsocks-2022 method.
passwordstringyes—Legacy methods: the shared password, from which the key is derived. It is ignored when users is not empty, but the key must still be present. Shadowsocks 2022: a base64 pre-shared key (standard alphabet with padding, surrounding whitespace trimmed) of 16 bytes for 2022-blake3-aes-128-gcm and 32 bytes otherwise. When users is not empty, this is the identity PSK (iPSK) that every client puts first in its password. A key that is too short fails with shadowsocks-2022: PSK too short; a longer key is reduced to the first 16 or 32 bytes of its SHA-256 digest.
usersarray of tablesno[]Users on one port, each written as { password = "…", email = "…" }. clients is accepted as an alias, but not together with users. Empty means a single shared password or PSK. For Shadowsocks 2022 multi-user needs an AES-GCM method; with 2022-blake3-chacha20-poly1305 the config fails with shadowsocks-2022: multi-user requires an aes-gcm method.
users[].passwordstringyes—Legacy methods: this user's password. Shadowsocks 2022: this user's base64 PSK (uPSK), with the same length rules as password. Give every user a different value.
users[].emailstringno""A label for the user. etemenanki-app accepts it so that configs copied from Xray work as they are, but does not use it: no route rule matches on it, and no log line prints it.

The settings table is strict. A key that is not listed here, such as Xray’s network, level or a per-client method, fails with invalid settings: unknown field.

The inbound listens on TCP only, and takes no transport. An [inbound.stream] is accepted only when it asks for plain TCP: network unset, empty or "tcp", and security unset, empty or "none". Any other network, or security = "tls", fails with protocol shadowsocks does not support stream network "…" or … stream security "…". If you need Shadowsocks behind TLS or WebSocket on the server side, use a protocol that supports a transport instead, such as Trojan or VLESS.

With no users, the whole port shares one secret.

[inbound.settings]
method = "2022-blake3-aes-128-gcm"
password = "AAAAAAAAAAAAAAAAAAAAAA==" # openssl rand -base64 16

Clients use the same method and the same key as their password.

List the users in users (or clients). Each user has their own password and an optional email label. The families find the user in different ways, and they give the top-level password a different meaning.

[inbound.settings]
method = "2022-blake3-aes-256-gcm"
password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # iPSK
[[inbound.settings.users]]
password = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=" # alice's uPSK
email = "alice@example.com"
  • The top-level password becomes the identity PSK (iPSK). It is shared by every user, and on its own it no longer lets anyone in.
  • Each user’s password is that user’s uPSK.
  • Each client sets its password to <iPSK>:<uPSK>.
  • Multi-user needs 2022-blake3-aes-128-gcm or 2022-blake3-aes-256-gcm. With 2022-blake3-chacha20-poly1305 the config fails with shadowsocks-2022: multi-user requires an aes-gcm method, because the SIP022 identity header is encrypted with AES.

A legacy connection carries nothing that names the user, so the server tries each user’s key on the first encrypted chunk, in list order, and takes the first one that decrypts it. If two users share a password, the first of them always wins. The work per new connection grows with the number of users.

A 2022 connection names its user. The client puts an identity header after the salt: a hash of its uPSK, encrypted with a key derived from the iPSK. The server decrypts it and looks the hash up in a table, so the number of users does not change the cost:

flowchart LR
  C["client: password = iPSK:uPSK"] -->|"salt + identity header + request"| D["server decrypts the header with the iPSK"]
  D --> L{"hash of a configured uPSK?"}
  L -->|yes| S["session key from that user's uPSK"]
  L -->|no| X["connection closed"]
  S --> R["request decrypted, flow opened"]

Give every user a different uPSK. If two users share one, the server cannot tell them apart and treats both as the later entry.

Users live in the config file. To add or remove one, edit the file and reload; see Hot reload.

protocol = "shadowsocks" in an [[outbound]] makes etemenanki-app a Shadowsocks client. server and port are required and name the Shadowsocks server; address_family controls how its name is resolved. These common keys are described on Outbounds. The protocol keys go under [outbound.settings]:

KeyTypeRequiredDefaultDescription
methodstring (enum)yes—The cipher, with the same accepted values as the inbound: a 2022- value must be exactly 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm or 2022-blake3-chacha20-poly1305; any other value is a legacy AEAD method or alias, matched case-insensitively. It must match the server. An unknown value fails with unknown shadowsocks method or unknown shadowsocks-2022 method.
passwordstringyes—Legacy methods: the password, used as written (a : has no special meaning). Shadowsocks 2022: either one base64 PSK for a single-PSK server, or a chain iPSK:uPSK for a multi-user server, where the last key is your own user key and every key before it is an identity key. Each key follows the inbound length rules, and a key that is too short fails with shadowsocks-2022: PSK too short.

Unlike the inbound, the outbound has no users: one outbound is one user. users, clients or any other key fails with invalid settings: unknown field.

The shape of password depends on how the server is set up:

Server Client password
One PSK, no users "<PSK>"
Several users "<iPSK>:<uPSK>"

The outbound splits password on :. The last key is the user key, used for the session. Every key before it is an identity key, and the outbound writes one identity header per identity key, in order. It accepts longer chains such as iPSK1:iPSK2:uPSK, but an etemenanki-app server reads exactly one identity header, so give it exactly two keys.

Only the AES-GCM methods support identity headers. The outbound does not reject a chain with 2022-blake3-chacha20-poly1305, but an etemenanki-app server does not accept one.

A legacy password is used as written, so a : in it is an ordinary character.

The outbound can run Shadowsocks inside any client transport: TLS, WebSocket or gRPC, set in [outbound.stream] (see Transports). This is for servers that put Shadowsocks behind such a transport. The etemenanki-app inbound cannot be that server.

Shadowsocks over WebSocket and TLS
[[outbound]]
tag = "ss-ws"
protocol = "shadowsocks"
server = "proxy.example.com"
port = 443
[outbound.stream]
network = "ws"
security = "tls"
[outbound.stream.ws]
path = "/ss"
[outbound.settings]
method = "aes-256-gcm"
password = "replace-with-a-long-random-password"

Neither the inbound nor the outbound carries UDP, in either family.

  • The inbound binds a TCP listener only. Clients that relay UDP over Shadowsocks get no answer, so turn off UDP relay in the client, or send UDP some other way.
  • A UDP flow that the router sends to a Shadowsocks outbound fails with shadowsocks carries no datagrams (legacy) or shadowsocks-2022 carries no datagrams, logged at debug level, and its packets are dropped. Add a network = "udp" rule that sends UDP elsewhere, placed before any rule that could send UDP to the Shadowsocks outbound. The client example above does this; it matters there because the first outbound is also the default for everything no rule matches. See Routing.

Every Shadowsocks 2022 request and response carries a Unix timestamp. The server refuses a request whose timestamp is more than 30 seconds from its own clock, and an etemenanki-app client refuses a response on the same terms. Keep the clocks of both machines synchronised, for example with NTP. The legacy family has no timestamp.

When the server cannot decrypt a connection, cannot match it to a user, or finds its timestamp out of range, it closes the connection without sending anything. The reason is logged at debug level (set [log] level = "debug"), in the form shadowsocks-2022 connection from Some(198.51.100.7) ended: proxy core: shadowsocks-2022: bad timestamp. A legacy connection that matches no user logs shadowsocks connection from … ended: proxy core: shadowsocks: no matching user, and a 2022 connection whose identity header names no configured user logs … proxy core: shadowsocks-2022: unknown identity.

Errors about 2022 keys (PSK too short, decode PSK, multi-user requires an aes-gcm method) do not name the inbound or outbound they come from. If a config has several Shadowsocks entries, check the keys of each.

Message Cause Fix
inbound ss-in: unknown shadowsocks method "none" The method is not an AEAD cipher. none, plain and stream ciphers are not supported. Use one of the methods in Ciphers.
inbound ss-in: unknown shadowsocks-2022 method "2022-BLAKE3-AES-128-GCM" 2022 names are case-sensitive, and the value is not trimmed. Write the name exactly, in lower case, with no spaces.
shadowsocks-2022: PSK too short (16 < 32) The key decodes to 16 bytes but the method needs 32. Generate a key with openssl rand -base64 32, or use 2022-blake3-aes-128-gcm.
shadowsocks-2022: PSK too short (0 < 16) The 2022 password is empty, or a chain has an empty part, such as a trailing :. Fill in every key.
decode PSK: Invalid symbol 45, offset 3. The key is not standard base64. Here 45 is -, from a URL-safe key or a plain password. Use standard base64, for example from openssl rand.
decode PSK: Invalid padding The key has lost its trailing = characters. Copy the whole key, padding included.
shadowsocks-2022: multi-user requires an aes-gcm method users is set with 2022-blake3-chacha20-poly1305. Switch to an AES-GCM 2022 method, or remove users.
inbound ss-in: invalid settings: missing field `password` password is missing, which also happens when users is set. Add a top-level password. For legacy multi-user its value is not used.
inbound ss-in: invalid settings: duplicate field `users` Both users and clients are present. Keep one of them.
inbound ss-in: protocol shadowsocks does not support stream network "ws" The inbound has an [inbound.stream] with a transport. Remove it. Only the outbound supports transports.
udp fan-out: opening an outbound failed: shadowsocks-2022 carries no datagrams (debug log) A UDP flow was routed to a Shadowsocks outbound. Route UDP to another outbound with a network = "udp" rule.