Throwing rocks against the wall
Shadowrocks is a shadowsocks
port written in pure async/.await
Rust.
At the moment it only does the basics: tunneling from a local SOCKS5 server to a remote server, with
proper encryption. The implementation is thoroughly tested and is compatible with the original
python version with --compatible-mode
.
The official Rust implementation of shadowsocks
can be found here. It has way more
functionality.
shadowrocks
is better in several aspects.
- It is thoroughly tested. There is even a test for replay attack mitigation.
- Improved encryption, see Compatibility.
- It uses
async-trait
to simplify implementation. No morefn poll_write(self: Pin<&mut Self>, ctx: &mut Context<'_>, buf: &[u8])
.
To start the local SOCKS5 server at port 51980
, run
cargo run -- -l 51980 -s 9.9.9.9 -p 51986 -k test-password
In the meantime, start the remote shadow server (e.g. on a VPS with public IP
9.9.9.9
) by running
cargo run -- --shadow -s 9.9.9.9 -p 51986 -k test-password
The server address (-s
), server port (-p
) and password (-k
) flags must match.
JSON configuration files (-c
) and ss://
URLs (--server-url
) described in
SIP002 are also supported.
Five types of ciphers are supported:
chacha20-ietf-poly1305
provided by sodiumxchacha20-ietf-poly1305
provided by sodiumaes-128-gcm
by OpenSSLaes-192-gcm
by OpenSSLaes-256-gcm
by OpenSSL
All of them are AEAD ciphers.
In non-compatible mode, a few changes are made to the traffic between the socks server and shadow server.
- Master key is derived using
PBKDF2
, as opposite toPBKDF1
used in the original version. Master key is still derived from the password. - Sub-keys are derived using
HKDF
withSHA256
, instead ofSHA1
, which is no longer considered secure. The input key toHKDF
is still the master key. - During encryption handshake, the salt used by the socks server to encrypt outgoing traffic is designated by the shadow server, while the salt used by the shadow server is designated by the socks server. This is the opposite to the original version, where each server decides their own salt.
Item #3 helps defend against replay attacks. If we can reasonably assume that salt generated is different each time, then both servers have to re-encrypt traffic for every new connection. Attackers will need to derive a different sub-key for the replied session, which cannot be done without the master key.
In compatible mode, shadowrocks
behaves the same as the original version.
- TCP tunneling
- Integrate Clippy
- Benchmarks
- Integration testing
- Crate level documentation
- Document the code in
src/crypto
in detail - UDP tunneling with optional fake-tcp
- Replay attack mitigation in compatible mode
- Replay attack mitigation in non-compatible mode
- Native obfuscation
- Manager API to create servers on the fly
-
ss://
URL and JSON config file
Both the ring
crate (BoringSSL) and the openssl
crate are used.
The functionality largely overlaps between those two. ring
was originally
used as a reference point and sanity check to openssl
, when the author is
unfamiliar with the crypto used in shadowsocks
.
features | ring |
openssl |
---|---|---|
PBKDF1 |
✅ | |
PBKDF2 |
✅ | ✅ |
HKDF-SHA1 |
✅ | ✅ |
HKDF-SHA256 |
✅ | ✅ |
AES-128-GCM |
✅ | ✅ |
AES-192-GCM |
✅ | |
AES-256-GCM |
✅ | ✅ |
HKDF-SHA1
support was recently added to ring
.
The ring
crate can be disabled by disabling feature ring-crypto
. The
openssl
crate cannot be completely disabled at the moment.
- Reduce memory allocation in the encryption / decryption process.
The current implementation does a lot of small memory allocations for each connection. For example, to send the SOCKS 5 address from socks server to remote shadow server, the following process is followed.
- Turning SOCKS 5 address into bytes.
- Turning packet length into bytes (
x
bytes,x = 2
). - Turning nonce into bytes (4 bytes).
- An OpenSSL crypter object.
- Ciphertext of packet length (
x
bytes,x = 2
). - Tag for the encryption (16 bytes).
- Concatenation of ciphertext and tag (18 bytes).
- Repeat 2-7 for SOCKS5 address with
x
varies.
To summarize, 13 allocations for each packet. Each encryption costs 6 allocations, and each packet we have to encrypt twice: once for packet length and once for the actual information.
The process for reading is similar.
- Ciphertext of packet length.
- Tag for encryption
- Turning nonce into bytes
- An OpenSSL crypter object.
- Packet length plaintext.
- Repeat 1-5 for packet content.
We saved one step for the "ciphertext without tag" part. Nonetheless this is still terrible.