Shadowsocks to Tor: why it failed as a VPN alternative
Well, the time has come, and I’ve decided to update, clean up, and generally re-think my “home” infrastructure. This part is only about a (mostly failed) networking setup.
What do I have and what do I want? I have a few virtual private servers (VPS) from different hosting providers, a Raspberry Pi, a PlayStation, some PCs at home, and mobile phones. The goal is to set up simple monitoring of the VPSs, build a dashboard on the Raspberry Pi, and have the ability to deploy services without constantly worrying about security. I also want to occasionally access the internet from third-party regions and IP addresses, with the ability to add more infrastructure without pain. Something similar to the scheme below:

And because it’s a truly multi-tenant setup involving many different internet
service providers, I want to make it reliable and free from ISP spying. That’s
why I explored using Shadowsocks (specifically sslocal in TUN mode) to
create a VPN-like routed network. A main selling point of Shadowsocks is its
ability to circumvent censorship, which aligns with my requirements. So, here’s
the story (as there isn’t much information available about Shadowsocks in this
particular action).
For those in a hurry, here are the prepared Ansible playbooks: https://github.com/irr123/shadowsocks-to-tor
# Tor daemon
I started with the simplest part – setting up Tor. Tor needs no long intro; at least the Tor Browser is familiar enough. In my case, I only needed to set up the daemon (without the browser) and route the VPN’s external traffic through it. (Spoiler: this part works almost perfectly).
# Tor config
According to https://support.torproject.org/apt/tor-deb-repo/, after installing the necessary packages, the daemon starts automatically.
Before installation, I write the config to /etc/tor/torrc:
AutomapHostsOnResolve 1
AutomapHostsSuffixes .onion,.exit
AvoidDiskWrites 1
DNSPort 127.0.0.1:9053
TransPort 127.0.0.1:9040
SocksPort 127.0.0.1:9050
Checking:
torify curl https://check.torproject.org | grep "Congrat"
...
Congratulations. This browser is configured to use Tor.
Simple enough, Tor is too mature technology to introduce issues in default configuration.
# Shadowsocks
I’m using the Rust implementation, which consists of several components (and it’s quite problematic to initially clarify what each of them is responsible for, based on their docs):
- ssserver: Actually a service that accepts client connections and forwards traffic externally.
- sslocal: A client, like the one found at https://github.com/shadowsocks/shadowsocks-windows/releases.
- ssmanager: A utility that allows for dynamic management of server instances and provides some observability statistics.
- ssservice: A unified entrypoint to manage all previous commands (?), plus a password generator.
- ssurl: A utility to generate links like ss://ENCODED_CONFIG@SERVER_ADDRESS:SERVER_PORT, which can then be encoded into a QR-code and easily applied on a mobile phone, for example.
# Shadowsocks server config
The docs provide many different
installation options,
from regular Linux repos and snaps to Docker images and self-built binaries. I
decided to use pre-built binaries from github releases page. The same applies to
managing the service lifecycle – systemd, supervisord, self-managed,
Docker/k8s; systemd is my choice. Here’s the systemd unit file at
/etc/systemd/system/shadowsocks-server.service:
[Unit]
Description=Shadowsocks-rust Server Service
Documentation=https://github.com/shadowsocks/shadowsocks-rust
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ssuser
Group=ssuser
ExecStart=/opt/shadowsocks/v1.23.4/ssserver -c /opt/shadowsocks/v1.23.4/config.json
WorkingDirectory=/opt/shadowsocks/v1.23.4
LimitNOFILE=51200
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
An important part here is the dedicated user. Create it manually, as this user will be referenced in the iptables rules implemented later:
useradd --system --shell /usr/sbin/nologin --no-create-home ssuser
Additionally, I need /opt/shadowsocks/v1.23.4/config.json (exact locations are
my choice):
{
"server": "0.0.0.0",
"server_port": 8388,
"local_port": 1080,
"password": "YOUR_GENERATED_PASSWORD_HERE",
"method": "chacha20-ietf-poly1305",
"mode": "tcp_and_udp"
}
First, generate the password by running (it’s included with the shadowsocks-rust binaries):
ssservice genkey --encrypt-method chacha20-ietf-poly1305
Then, copy the output and paste it as the value for the “password” field above.
Enable and start the service; that’s almost all:
systemctl enable shadowsocks-server.service
systemctl start shadowsocks-server.service
After this, I can configure any client, like the Windows one I mentioned previously. It passes traffic through the set-up VPS. Configure a local/global/PAC SOCKS5 proxy and try myip.wtf. With the server set up like this, the next step is to route its outgoing traffic through Tor.
Don’t forget, all these parts are already automated by the Ansible playbook.
# Connecting Shadowsocks with Tor
Now I complete the setup by applying these iptables rules on the VPS running ssserver and Tor:
# Create a new chain for Tor output
iptables -t nat -N TOR_OUTPUT || /bin/true
# Route all output from the 'ssuser' to our new TOR_OUTPUT chain
iptables -t nat -A OUTPUT -m owner --uid-owner ssuser -j TOR_OUTPUT
# Exclude private/reserved networks from Tor redirection (adjust as needed)
iptables -t nat -A TOR_OUTPUT -d 0.0.0.0/8 -j RETURN
iptables -t nat -A TOR_OUTPUT -d 10.0.0.0/8 -j RETURN
iptables -t nat -A TOR_OUTPUT -d 172.16.0.0/12 -j RETURN
iptables -t nat -A TOR_OUTPUT -d 192.168.0.0/16 -j RETURN
iptables -t nat -A TOR_OUTPUT -d 127.0.0.0/8 -j RETURN # Localhost traffic
iptables -t nat -A TOR_OUTPUT -d <VPS public address> -j RETURN # Traffic to VPS itself
# Redirect TCP traffic
iptables -t nat -A TOR_OUTPUT -p tcp -m tcp --syn -j REDIRECT --to-ports 9040
# Redirect DNS UDP
iptables -t nat -A TOR_OUTPUT -p udp --dport 53 -j REDIRECT --to-ports 9053
# Persist the rules
netfilter-persistent save
Now I recheck https://myip.wtf through the Shadowsocks client and should see an IP address from the Tor network.
# Where Shadowsocks failed as a VPN
In general, this might be a good enough setup for particular cases. For example, when checking https://dnsleaktest.com, DNS requests are not leaking through the Shadowsocks-Tor setup. WebRTC, on the other hand, could expose my real address; potential solutions might involve aggressive measures like blocking all UDP traffic, which I wasn’t prepared to do.
Another disadvantage – this setup doesn’t reliably resolve .onion addresses
for the end client, while on the VPS it works. On the client, it somehow fails.
(I didn’t dig too deeply into this because of a more significant issue for me).
Fun and less important, but confusing - the Shadowsocks icon for some clients is too similar to the Telegram icon. Gets me every time.
And the most important issues for me:
- Inter-Client Connectivity: The server provides problematic connectivity between clients set up in TUN mode. For example, in the Ansible playbook, I set up Linux clients in TUN mode in their own subnet (192.168.7.0/24). While ICMP (ping) worked between them, neither TCP nor UDP data packets were reliably passed end-to-end from one Shadowsocks client (e.g., my main server trying to reach another VPS also connected via sslocal) to another. TCP connections would appear to establish on the initiating client’s TUN interface, but no actual data would reach the destination client.
- Client Limitations: The standard Windows client (and many others) doesn’t provide a system-wide VPN service; it’s only a SOCKS5/HTTP proxy. This means that I can only access the internet through applications configured to use the proxy. There’s no direct access to other devices in a private network (e.g., 192.168.7.x) if I’m trying to build one, nor does it tunnel all system traffic.
# Verdict: proxy works, VPN doesn’t
As I mentioned at the start, while the Shadowsocks protocol itself is likely great for its core purpose (circumventing DPI and proxying traffic), the standard ssserver tool isn’t designed to build a fully-featured, routed private network between all connected clients. It excels as a secure proxy, but for creating a VPN where all devices can seamlessly communicate as if on a local network, it falls short.
Or at least, I didn’t find a solution using Shadowsocks alone to achieve all my networking goals for this internal infrastructure. Reverting WireGuard 😌