GFW Technical Review 05 – Shadowsocks

In 2012, a developer known as “clowwindy” released Shadowsocks – a lightweight proxy protocol designed specifically for circumventing the GFW. It was initially a simple tool, only a few hundred lines of Python. But it was fast, lightweight, and most importantly, more resilient against GFW than typical VPNs. It quickly caught the attention of the community. Over the following years, it would become the most influential circumvention tool of the 2010s and fundamentally shape the cat-and-mouse game between 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 have well-known protocol signatures that GFW can easily fingerprint, Shadowsocks was designed from the ground up to be indistinguishable from random noise – a “looks like nothing” protocol. It eschewed the complexity of established protocols like OpenVPN or IPsec in favor of minimalism: a thin encrypted tunnel with no discernible handshake pattern, no fixed header structure, and no protocol-level metadata in plaintext that could serve as a fingerprint.

Shadowsocks is also fast and lightweight. It is stateless, requiring no handshakes or key exchanges. It operates as a SOCKS5 proxy on the application layer, rather than as a heavyweight network-layer solution like a VPN.

This focus on obfuscation and simplicity made Shadowsocks popular very quickly. It was released as open-source software with a clean, readable implementation. Setting up a server required minimal technical expertise. Configuration was straightforward – just a simple JSON file – and the protocol was lightweight enough to run on cheap VPS instances. This democratized circumvention: anyone with a few dollars a month could operate their own private proxy, making large-scale blocking far more difficult 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. GFW only observes the fully encrypted data 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 – hence the IV adds randomness to ensure different encryptions of the same data 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 only confidentiality, not authenticity. For a man-in-the-middle attacker like GFW, stream ciphers guarantee that they cannot decrypt the communication, but they can still flip bits to modify the plaintext without detection.


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. This is followed by an encrypted length field, which is necessary because AEAD encrypts data in discrete chunks, each terminated by an authentication tag – the length field allows the receiver to locate chunk boundaries. A 16-byte length tag protects the integrity of the length field. Then comes the Shadowsocks payload (ATYP, destination address, port, and actual data), followed by a 16-byte payload tag. This structure introduces 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 using 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 and 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 cipher implementations but share the same overall principles outlined above. The cryptographic internals of these ciphers are beyond the scope of this blog.


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 GFW, and GFW doesn’t randomly flip bits in packets – it either blocks connections or lets them through. Stream ciphers are also theoretically weaker against certain attacks like replay attacks (which we’ll cover in future posts), but this didn’t 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 the attack works. First, the attacker captures the encrypted traffic of a Shadowsocks connection – certainly feasible for GFW. The attacker then sets up their own ss-local client and replays this 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 simply 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 GFW’s perspective, Shadowsocks traffic appears to be random bytes.

This is Shadowsocks’ greatest strength – but also its weakness. True, GFW cannot identify Shadowsocks through plaintext fields or handshake patterns. However, very few protocols on the Internet are fully encrypted from the first byte. Even secure protocols like TLS have handshake and key exchange phases with specific patterns and plaintext fields. Being fully encrypted is itself suspicious.

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

  1. Entropy test: GFW measures packet entropy. High-entropy data flows are likely fully encrypted and therefore suspicious. GFW uses popcount(pkt) / len(pkt) to quantify entropy; values between 3.4 and 4.6 are flagged as suspicious.
  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 to 4 all test whether a packet is likely to be fully encrypted. Typically, all five criteria must be met to trigger blocking – but there are likely additional undiscovered rules, and GFW probably evaluates multiple metrics holistically.

Another fingerprinting 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.

Remarkably, after nearly 15 years of GFW evolution, Shadowsocks remains an effective and widely-used circumvention tool – though newer, more resilient protocols have also emerged. Shadowsocks itself has gone through various iterations as GFW introduced new 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 08 – Tor
  • GFW Technical Review 07 – Active Probing
  • GFW Technical Review 06 – HTTPS and Domain Fronting
  • GFW Technical Review 04 – The West Chamber Project
  • GFW Technical Review 03 – Deep Packet Inspection