GFW Technical Review 05 – Shadowsocks

In 2012, a developer known as “clowwindy” released Shadowsocks: a lightweight proxy protocol designed to circumvent the GFW. It started as a simple tool, only a few hundred lines of Python, but it was fast and more resilient against the GFW than typical VPNs. The community noticed quickly. Over the following years, Shadowsocks became the most influential circumvention tool of the 2010s and fundamentally shaped the cat-and-mouse game between the GFW and circumvention developers.


Design Philosophy

Shadowsocks emerged from a simple insight: the best way to evade detection is not to look suspicious in the first place. Unlike VPNs, which carry well-known protocol signatures the GFW can fingerprint, Shadowsocks was designed from the ground up to be indistinguishable from random noise, a “looks like nothing” protocol. It eschewed the complexity of OpenVPN or IPsec in favor of minimalism: a thin encrypted tunnel with no discernible handshake pattern, no fixed header structure, and no plaintext protocol metadata that could serve as a fingerprint.

Shadowsocks is also stateless: no handshakes, no key exchanges. It operates as a SOCKS5 proxy at the application layer, not a heavyweight network-layer solution like a VPN.

This focus on obfuscation and simplicity made Shadowsocks popular almost immediately. The implementation was open source and readable; setting up a server took minimal expertise; configuration was a simple JSON file; the protocol was light enough to run on cheap VPS instances. This democratized circumvention: anyone with a few dollars a month could run their own private proxy, making large-scale blocking far harder than targeting centralized VPN services.


Architecture

Shadowsocks has a straightforward architecture consisting of two components: a client-side application (ss-local) and a server-side service (ss-server).

ss-local runs on the user’s machine as a standard SOCKS5 proxy (see RFC 1928), accepting connections from local applications such as web browsers. It encrypts the data stream and forwards it to the configured ss-server, which runs on a host outside the censored network. ss-server decrypts the payload, reconstructs the original connection parameters, and relays requests to the intended destination. The GFW only ever sees the fully encrypted stream between the two Shadowsocks components.


The Shadowsocks Protocol

Shadowsocks is a stateless protocol. There is no concept of a persistent connection, no handshake, and no key exchange phase. Payload transmission begins as soon as a TCP connection is established between ss-local and ss-server. Cryptographic security relies entirely on a pre-shared secret (password) configured statically on both components.

A Shadowsocks packet begins with an Initialization Vector (IV): a random value used as a salt in the encryption process. The IV is between 8 and 16 bytes depending on the cipher scheme, and it is the only field in the packet that is not encrypted.

The encrypted portion begins with a 3-tuple identifier for the target destination: a 1-byte ATYP field defining the address type, a variable-length ADDR field (which can be an IPv4 address, IPv6 address, or domain name), and a 2-byte destination port. Note that this header information is only sent in the first packet; ss-server retains these values for subsequent packets in the same connection.

The first packet structure looks like:

ATYP Type Address Length
0x01 IPv4 4 bytes
0x03 Domain 1-byte length + domain string
0x04 IPv6 16 bytes

Stream Cipher

Shadowsocks’ original encryption method uses stream ciphers. First, Shadowsocks derives a master key from the pre-shared password using a process similar to OpenSSL’s EVP_BytesToKey:

key = MD5(password) ++ MD5(MD5(password) ++ password) ++ ...

where ++ denotes concatenation. This derivation is repeated until enough key bytes are generated for the chosen cipher. Next, the master key and IV are fed into a keystream generator to produce a stream of pseudorandom bytes. Each byte of the keystream is XORed with the corresponding byte of the plaintext to produce ciphertext. Because both ends share the same master key and IV, they generate identical keystreams and can perform the same XOR operation for both encryption and decryption. Since this process is deterministic, using the master key alone would produce identical ciphertext from identical plaintext. The IV adds the randomness needed for different encryptions of the same data to yield different ciphertext.

Shadowsocks supports over a dozen stream cipher schemes with varying security and performance characteristics, but they all follow this same fundamental process, just with different keystream generators.

A key drawback of stream ciphers is that they provide confidentiality but not authenticity. For a man-in-the-middle attacker like the GFW, stream ciphers guarantee the communication cannot be decrypted, but they do not stop the attacker from flipping bits to modify the plaintext undetected.


AEAD

AEAD (Authenticated Encryption with Associated Data) adds authenticity on top of confidentiality. This is achieved by appending an authentication tag that allows the cipher to verify payload integrity.

An AEAD-mode Shadowsocks packet has a different wire format. It begins with a 32-byte salt (analogous to the IV in stream cipher mode), which appears only in the first packet of a TCP stream. Next is an encrypted length field, needed because AEAD encrypts data in discrete chunks each terminated by an authentication tag; the length field tells the receiver where chunk boundaries fall. A 16-byte length tag protects the length field’s integrity. Then comes the Shadowsocks payload (ATYP, destination address, port, and actual data), followed by a 16-byte payload tag. This adds significant size overhead compared to stream ciphers, especially for small packets.

Note that the length and length tag fields are only required in TCP mode. TCP is a stream protocol that may segment and reassemble data across packet boundaries, so explicit length information is needed. In UDP mode, each chunk corresponds to a single UDP datagram, so the length can be derived from the UDP header.

Shadowsocks TCP packet format in AEAD-mode

The AEAD encryption process works as follows. First, a master key is derived from the password using EVP_BytesToKey, just as with stream ciphers. Next, a 32-byte session key is derived by combining the master key and salt with HKDF-SHA1. This session key is used for the entire data stream. AEAD encrypts each plaintext chunk using the session key and a nonce (a counter starting at 0 that ensures unique encryption within the session). Each chunk requires two AEAD operations (one for the length field, one for the payload), so the nonce increments twice per chunk.

Shadowsocks AEAD Encryption

Given the deterministic nature of key and nonce derivation, decryption works similarly in reverse. The authentication tags now allow the receiver to validate data integrity and discard any packets that fail verification.

Shadowsocks supports multiple AEAD ciphers, such as AEAD_CHACHA20_POLY1305 and AEAD_AES_256_GCM. These are different implementations but share the same overall principles outlined above. The cryptographic internals are beyond the scope of this post.


The Redirect Attack

For a long time, the lack of authenticity in stream ciphers was not considered a serious concern. After all, Shadowsocks was designed to circumvent the GFW, and the GFW does not randomly flip bits in packets; it either blocks connections or lets them through. Stream ciphers are also theoretically weaker against attacks like replay (which we’ll cover in future posts), but that did not seem to matter in practice. Up until 2020, most Shadowsocks deployments still used stream ciphers without AEAD.

This changed when a devastating redirect attack was discovered in 2020, allowing an attacker to fully decrypt recorded Shadowsocks traffic without knowing the password.

Here’s how it works. The attacker captures the encrypted traffic of a Shadowsocks connection (well within the GFW’s reach), spins up their own ss-local, and replays the captured traffic to the original ss-server. Crucially, the attacker modifies the target address field to point to a server they control. Since stream ciphers do not guarantee data integrity, ss-server happily decrypts the data and relays it to the attacker’s server:

ss-local (attacker-controlled) <--[encrypted]--> ss-server <--[plaintext]--> target (attacker-controlled)

The challenge is modifying the target address without knowing the password. The attacker cannot encrypt their desired target address into valid ciphertext directly. However, if the attacker can guess the plaintext of certain bytes in the ciphertext, they can alter those bytes to arbitrary values. This follows from the mathematical properties of stream ciphers.

Let c be the ciphertext, p be the plaintext, and k be the keystream. Stream cipher encryption is simply (where ⊕ denotes XOR):

c = p ⊕ k

Now suppose the attacker wants to change p to p’. They can compute r such that p' = p ⊕ r. The corresponding valid ciphertext c’ would be:

c' = p' ⊕ k = (p ⊕ r) ⊕ k

The attacker doesn’t know k, so they cannot compute this directly. However, XOR is both associative and commutative:

c' = (p ⊕ r) ⊕ k = (p ⊕ k) ⊕ r = c ⊕ r

So the attacker can construct valid ciphertext c’ by XORing the original ciphertext with r, without ever knowing the keystream.

To perform this attack, the attacker needs a packet where they can guess the first 7 bytes, allowing them to replace the address header with one pointing to their server. One simple approach exploits the fact that HTTP responses always begin with “HTTP/1.”. The attacker takes a server-to-client packet (which lacks Shadowsocks’ address header), assumes it’s an HTTP response, and performs the bit-flipping attack. They then construct an attack packet using the original IV plus the modified payload and send it to ss-server.

The attacker may not immediately find an HTTP response, but they can try different captured packets until they observe a connection to their target server. This establishes a decryption oracle: they can now replay any captured encrypted packets through this channel to obtain the plaintext. Throughout this entire attack, the attacker never learns the password.


Fingerprinting Shadowsocks

As we’ve seen, both stream cipher and AEAD packets are fully encrypted, aside from the initial IV or salt (which are themselves random values). From the GFW’s perspective, Shadowsocks traffic looks like random bytes.

This is Shadowsocks’ greatest strength and also its weakness. The GFW cannot identify Shadowsocks through plaintext fields or handshake patterns, true. But very few protocols on the Internet are fully encrypted from the first byte. Even TLS has handshake and key exchange phases with specific patterns and plaintext fields. Being fully encrypted is itself suspicious.

The GFW fingerprints Shadowsocks (and other fully encrypted protocols) through a combination of heuristics. Researchers have identified at least five rules:

  1. Entropy test: the GFW measures packet entropy as popcount(pkt) / len(pkt) (bits set per byte). High-entropy flows are likely fully encrypted; values in the range 3.4–4.6 are flagged.
  2. First-byte test: Whether the first 6 bytes are printable ASCII characters (0x200x7E).
  3. Printable ratio test: Whether more than 50% of the packet consists of printable characters.
  4. Contiguous printable test: Whether more than 20 contiguous bytes are printable characters.
  5. Protocol exemption: Whether the connection matches TLS or HTTP fingerprints. Genuine TLS or HTTP connections are exempted.

Rules 1 through 4 all test whether a packet looks fully encrypted. Typically all five criteria must be met to trigger blocking, though additional undiscovered rules likely exist, and the GFW probably evaluates the metrics holistically.

Another method exploits packet length. Shadowsocks encryption, whether stream cipher or AEAD, does not alter payload length; only the protocol itself adds bytes for the address header and AEAD tags. The length of the first packet’s underlying traffic is often predictable, as most connections begin with TLS ClientHello or HTTP requests, which have characteristic size distributions.


Closing Thoughts

In 2015, clowwindy ceased development of Shadowsocks due to pressure from authorities. The open-source community took over the project but fragmented into multiple implementations: shadowsocks-libev, shadowsocks-rust, shadowsocks-go, and others.

After nearly 15 years of the GFW’s evolution, Shadowsocks remains an effective and widely used circumvention tool, though newer and more resilient protocols have since emerged. Shadowsocks itself has gone through several iterations as the GFW added detection capabilities, and the ongoing arms race continues to drive innovation on both sides.


References




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • GFW Technical Review 14 – The Cat and Mouse Game
  • GFW Technical Review 13 – Hysteria
  • GFW Technical Review 12 – Advanced TLS Evasion
  • GFW Technical Review 11 – Statistical Fingerprinting
  • GFW Technical Review 10 – Trojan