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.
Choosing a family
Section titled “Choosing a family”| 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 |
Quick start
Section titled “Quick start”This sets up a Shadowsocks 2022 server with two users, and an etemenanki-app client for one of them.
-
Generate one identity key for the server and one key per user.
2022-blake3-aes-256-gcmtakes 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) -
Put the keys into the server config and check it:
Terminal window etemenanki-app --test -c /etc/etemenanki/config.toml -
Give each user the password
<iPSK>:<uPSK>, their own key after the server’s identity key, together with the method, address and port.
# 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"]A local SOCKS5 proxy on 127.0.0.1:1080 that sends TCP through the server as alice, and UDP directly:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "ss-out"protocol = "shadowsocks"server = "proxy.example.com"port = 8388
[outbound.settings]method = "2022-blake3-aes-256-gcm"# "<iPSK>:<uPSK>": the server's identity key, then alice's own key.password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE="
[[outbound]]tag = "direct"protocol = "freedom"
# Shadowsocks carries TCP only, so send UDP somewhere that can carry it.[[route.rule]]network = "udp"outbound = "direct"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.
Ciphers
Section titled “Ciphers”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-GCMfails withunknown shadowsocks-2022 method. - Any other value is a legacy method, matched without regard to case, so
AES-256-GCMandCHACHA20-IETF-POLY1305work. - Neither kind is trimmed.
" aes-128-gcm"with a leading space fails. - There are no stream ciphers (
aes-256-cfb,rc4-md5and the like), and nononeorplainmethod. They fail withunknown 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.
Generating keys
Section titled “Generating keys”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.
Inbound settings
Section titled “Inbound settings”protocol = "shadowsocks" in an [[inbound]], with these keys under [inbound.settings]. The common inbound keys (tag, listen, port, sniffing) are described on Inbounds.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
method | string (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. |
password | string | yes | — | 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. |
users | array of tables | no | [] | 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[].password | string | yes | — | 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[].email | string | no | "" | 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.
One password or PSK
Section titled “One password or PSK”With no users, the whole port shares one secret.
[inbound.settings]method = "2022-blake3-aes-128-gcm"password = "AAAAAAAAAAAAAAAAAAAAAA==" # openssl rand -base64 16Clients use the same method and the same key as their password.
[inbound.settings]method = "chacha20-ietf-poly1305"password = "replace-with-a-long-random-password"Clients use the same method and password.
Several users on one port
Section titled “Several users on one port”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 uPSKemail = "alice@example.com"- The top-level
passwordbecomes the identity PSK (iPSK). It is shared by every user, and on its own it no longer lets anyone in. - Each user’s
passwordis that user’s uPSK. - Each client sets its password to
<iPSK>:<uPSK>. - Multi-user needs
2022-blake3-aes-128-gcmor2022-blake3-aes-256-gcm. With2022-blake3-chacha20-poly1305the config fails withshadowsocks-2022: multi-user requires an aes-gcm method, because the SIP022 identity header is encrypted with AES.
[inbound.settings]method = "aes-256-gcm"password = "unused-but-required"users = [ { password = "replace-with-a-long-random-password", email = "alice@example.com" }, { password = "replace-with-another-long-random-password", email = "bob@example.com" },]- The top-level
passwordis ignored onceusershas an entry, but the key must still be present. A client that uses it is refused. - Each client uses its own user’s password; there is no chain.
- Any legacy method works.
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.
Outbound settings
Section titled “Outbound settings”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]:
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
method | string (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. |
password | string | yes | — | 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.
Connecting to a 2022 server
Section titled “Connecting to a 2022 server”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.
Over a transport
Section titled “Over a transport”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.
[[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"Behaviour details
Section titled “Behaviour details”TCP only
Section titled “TCP only”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) orshadowsocks-2022 carries no datagrams, logged atdebuglevel, and its packets are dropped. Add anetwork = "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.
Clock skew (2022 only)
Section titled “Clock skew (2022 only)”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.
Failed connections
Section titled “Failed connections”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.
Key errors do not name the entry
Section titled “Key errors do not name the entry”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.
Common errors
Section titled “Common errors”| 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. |