Bypassing Deep Packet Inspection for VPN

When Wireguard and typical VPN gets blocked in your country and you're needing a little bit more...

Background

Following some civil unrests in my country, the government has slowly began to block off civilians from accessing a lot of sites. Unsurprisingly, people just got around this by simply downloading VPNs and using them to bypass government blockades. This worked for a while but then they started cracking down on VPNs too. Most of the free VPNs would simply refuse to work. For the paid apps, the government would setup checkpoints on the road where they would stop the cars and search the phones for VPN or other unwanted information and would be made to pay huge fines. This was something me and my family had to deal with and we would always have to delete the VPN apps when traveling downtown.

To bypass this, I deployed a wireguard server on Digital Ocean, and setup a Wireguard server which was cheaper then the monthly subscriptions that we were paying for VPNs. However, this didn't last long either as only a few months passed before the Wireguard connections were being throttled and sometimes not connecting at all.

I suspected that Wireguard connections were being blocked by DPI. Deep Packet Inspection (DPI) is a form of network packet filtering that examines the data part (and possibly also the header) of a packet as it passes an inspection point.DPI systems can recognize the patterns and signatures unique to Wireguard's protocol, even if the traffic is encrypted. This allows them to identify and block Wireguard connections. That is when I found ShadowSocks.

Solution

Shadowsocks is a secure socks5 proxy designed to protect your internet traffic. It was initially created by a Chinese developer to circumvent the Great Firewall of China, which imposes strict censorship on internet access. Shadowsocks works by setting up a proxy server. When you connect to the internet through this proxy, your internet traffic is encrypted and sent to the proxy server, which then forwards the traffic to its final destination. Shadowsocks is designed to make its traffic look like regular HTTPS traffic, which helps in evading DPI systems that block or throttle VPN traffic. A bonus is that Shadowsocks is less resource-intensive compared to traditional VPNs, resulting in faster performance and lower latency.

Configuring ShadowSocks server

First I created another droplet server on Digital Ocean with Ubuntu 24.04 LTS as the OS and with the data center nearest to my country

Creating a VPS on Digital Ocean

Next, I decided to go with lightest possible ShadowSocks implementation available which is the ShadowSocks-libev written in C#. There were few drawbacks like not having Workers - which handles multiple high traffic connections efficiently and also does not have Graceful Restart - which allows connections to be maintained even if the service was restarted.

My use case was for my family who does mostly browsing and social media so the traffic load wouldn't be high and a simple reconnection won't cause any significant issues for family use. A full table of the different implementations of ShadowSocks and its clients can be found here.

After setting up the server on Digital Ocean, I ssh into it. Then I installed the shadowsocks-libev:

sudo apt install shadowsocks-libev

Confirm to complete the installation. Next is to setup the password and ports. I use nano as my editor for simplicity but anything else is also fine.

sudo mkdir -p /etc/shadowsocks-libev
sudo nano /etc/shadowsocks-libev/config.json

This will open up config.json file similar to this:

sample_config.json
{
  "server": "0.0.0.0",
  "server_port": 8388,
  "local_port": 1080,
  "password": "yourstrongpassword",
  "timeout": 300,
  "method": "aes-256-gcm",
  "plugin": "",
  "plugin_opts": ""
}

Change the server port to a less known port. Put a strong and secure password.

Method is the encryption method used for the connection. I have tried many different methods and some did not work well with the android clients or are not supported at all. Some desktop clients also do not support all available encryption. The ones that works best for me is "chacha20-ietf-poly1305" and "aes-256-gcm" which is also the recommended one but feel free to try out all encryptions. You can find the link to all types of encryption here. After that, save your config.json file.

Now all you need to do is start the service:

sudo systemctl start shadowsocks-libev
sudo systemctl enable shadowsocks-libev

After that check if its running:

sudo systemctl status shadowsocks-libev

This should return something like this:

Active: active (running) since Tue 2024-06-16 20:33:40 UTC; 1 month 0 days ago

Setting up client

When I used Wireguard, wg-easy had a convenient interface allowing for me to easily generate QR codes. This was a bonus for me as my parents are not tech-savvy at all and typing the IP address and port number with passwords would greatly confuse them. However, this feature is not available in the shadowsocks-libev. However, I found a way to generate one through the client.

First, I downloaded ShadowSocks for Windows and ran the application. A grey icon resembling a paper-plane should appear in your system tray.

  1. Right click on the icon

  2. Go to servers

  3. Click on Edit Servers

  4. Select and delete the sample server that was given

  5. Click add

  6. Fill the ip address and password (in config.json) for your server

  7. Next select the encryption that you chose

  8. Click ok

  9. Now go back to ShadowSocks icon in the system tray, right click on it

  10. Go to System Proxy

  11. Choose Global

  12. This should now enable and redirect all your system traffic through the proxy

Check and make sure all the website and connections are working. I ran a few speedtests as well to see how much load my server was accumulation. It was hitting barely 15% with two simultaneous speedtest (around 250 Mbps). If you are satisfied with the tests its now time to generate QR code.

  1. Go to the system tray

  2. Right click on the ShadowSocks icon

  3. Select Edit Servers

  4. Click on Share Server Config

  5. Now a QR code should pop up in the menu

  6. Make sure you choose the right server and take a screenshot of the QR

  7. Now it can be shared to anyone and imported easily

  8. (Optional) but you can also share the server url instead it works on most Windows and Android devices.

For Multiple users and passwords

I only use one password and user for my family so multiple passwords and ports are not necessary but there is a way to do so. All you have to do is duplicate the config.json file and create another service. Shadowsocks is written in a way that there's very little memory overhead even if multiple processes are run.

After writing another config.json file lets say config1.json

config1.json
{
  "server": "0.0.0.0",
  "server_port": 8888,
  "local_port": 1081,
  "password": "yourstrongpassword",
  "timeout": 300,
  "method": "aes-256-gcm",
  "plugin": "",
  "plugin_opts": ""
}

Make sure your port and local port are different for each config files. Then run this command:

ss-server -c config1.json -f /var/run/ss-server1.pid
  • ss-server is the shadowsocks-libev service

  • -c config1.json is the path to the .json file

  • -f /var/run/ss-server1.pid is used to the pid (process number) of this specific instance. This is useful if you want to stop this specific instance for any reason

If you want to kill a specific shadowsocks instance all you have to do is run:

A side note: I had this error on the Journalctl

Too many open files

This means the amount of file descriptors available for this shadowsocks process was exceeded. This made to enter a loop where it continuously opens and closes each file descriptor trying to read the network packages resulting 100% CPU usage. To fix this I simply increased the amount file descriptors available for the shadowsocks process and CPU usage went down.

sudo ulimit -n 4096

For this code to work, I needed to stop and restart all my shadowsocks-libev process. This was made easy as I can just read the files from /var/run:

kill -SIGTERM $(cat /var/run/ss-server1.pid)

Then I just do htop to see if there's any lingering shadowsocks processes left before restarting each one

Generating QR Codes

If you have multiple config.json files, it gets a bit tedious to generate QR codes for each of them. I wrote bash script that would allow me to instantly generate QR codes for all the config.json files which can be used to scan and connect directly.

The prerequisites to run this is to have qrencode and jq libraries installed.

sudo apt-get install qrencode
sudo apt-get install jq

After installing create a text file with your favorite text editor and paste this script.

generate_qrcodes.sh
#!/bin/bash

# Fixed server IP
server_ip="your_server_ip"

# Folder containing configuration files
CONFIG_DIR=$(pwd)
# Folder to store QR codes
OUTPUT_DIR="${CONFIG_DIR}/qrcodes"

# Create output directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"

# Function to generate ss:// link and QR code for a single config file
generate_qr() {
    local config_file=$1
    local output_file="${OUTPUT_DIR}/$(basename "$config_file" .json).png"

    # Parse the configuration file for method, password, and port
    method=$(jq -r '.method // empty' "$config_file")
    password=$(jq -r '.password // empty' "$config_file")
    port=$(jq -r '.server_port // empty' "$config_file")

    # Check if any required value is missing
    if [[ -z "$method" || -z "$password" || -z "$port" ]]; then
        echo "Error: Missing required values in $config_file. Skipping QR generation."
        return
    fi

    # Create the ss:// link using the fixed server IP
    ss_link="ss://$(echo -n "${method}:${password}@${server_ip}:${port}" | base64)"

    # Generate the QR code from the ss:// link
    qrencode -o "$output_file" -t PNG "$ss_link"
    echo "Generated QR code for $config_file at $output_file"
}

# Check the argument passed to the script
if [[ $1 == "all" ]]; then
    # Generate QR codes for all config*.json files
    for config_file in "$CONFIG_DIR"/config*.json; do
        generate_qr "$config_file"
    done
elif [[ -f "$1" ]]; then
    # Generate QR code for the specified config file
    generate_qr "$1"
else
    echo "Usage: $0 all | <config.json>"
fi

Make sure the script is in the same folder where you store your config files. Add your server ip between the quotation marks for server_ip variable. You can change were the qr_cdoes are stored by specifying a different folder in OUTPUT_DIR

Then make the script file executable

chmod +x generate_qrcodes.sh

Now you can run it to generate all QR codes at once or by specifying a config

./generate_qrcodes.sh all 
./generate_qrcodes.sh config1.json

Last updated