Shadowsocks-over-WebSockets behind HTTPs website
This documentation is an extension of the existing WebSockets documentation. It adds a lot of additional information and components to enhance the setup, user experience, and obfuscation (via a bogus website).
Prerequisites
- a Linux server
- Operating System:
- Debian Linux
ℹ️ Note: in this document, Ubuntu is being used
- Debian Linux
- Platform:
- any 64-bit system
ℹ️ Note: in this document, a Raspberry Pi is used as the server (ARM64/AArch64)
- any 64-bit system
- Operating System:
- a non-root user that has sudo privileges
Step 1: Setup Website Domain and DNS
-
get your own domain from a domain registrar
ℹ️ Note: in this document, the domain
fake-website.comwill be used as the example domain that you purchase from a domain registrar for your bogus website -
[optional] if your server is in a network where the external IP is dynamic (occasionally changes), like your home, then get a Dynamic DNS (DDNS) hostname from a DDNS provider such as No-IP or DuckDNS
ℹ️ Note: Dynamic DNS (DDNS) maps a frequently changing (dynamic) IP address to a fixed hostname, allowing consistent remote access. By using a DDNS provider and a client (router or software), the system automatically updates DNS records, ensuring the hostname always points to the current IP address for services like home servers
ℹ️ Note: in this document, No-IP is used as the DDNS provider, and the domain
your.ddns-hostname.comwill be used as the example DDNS domain that you obtain from this DDNS provider -
configure your domain's DNS settings
- if your server has a static IP then:
- create an
Arecord in your DNS settings to pointfake-website.comto your static IPwherefake-website.com A XXX.XXX.XXX.XXXXXX.XXX.XXX.XXXis replaced by the server's IP
- create an
- [optional] otherwise, if your server has a dynamic IP and a DDNS hostname
then:
- create a
CNAMErecord in your DNS settings to pointfake-website.comtoyour.ddns-hostname.comfake-website.com CNAME your.ddns-hostname.com
- create a
- if your server has a static IP then:
Step 2: Initial Linux Server Setup
- SSH into your server
ℹ️ Note: in this document, the commands listed assume you are using a non-root user on the system that has sudo privileges
Step 2.1: Install OS Packages
-
configure APT package manager
-
edit or create this file:
/etc/apt/apt.conf.d/10sandboxexample:
sudo nano /etc/apt/apt.conf.d/10sandboxplace this into the file:
APT::Sandbox::User "root";ℹ️ Note:
APT::Sandbox::User "root";is a configuration directive used in Debian-based systems (like Ubuntu or Termux) to define which user account the APT package manager should use when performing sandboxed operations
-
-
update operating system (OS) and install packages
sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get install -y telnet \
wget \
curl \
netcat \
nmap \
yq \
qrencode \
nginx libnginx-mod-stream \
certbot python3-certbot-nginx
sudo apt-get clean
Step 2.2: [optional] Install & Configure DDNS DUC
-
if you setup a DDNS hostname then you will need to setup a Dynamic Update Client (DUC) to keep the hostname constantly up-to-date with your changing IP
-
the following steps show how to setup a No-IP DUC
-
download and unpack the DUC binaries
sudo mkdir -p /opt/noip-duc
cd /opt/noip-duc/
sudo wget https://dmej8g5cpdyqd.cloudfront.net/downloads/noip-duc_3.3.0.tar.gz
sudo tar xzf noip-duc_3.3.0.tar.gzℹ️ Note: at the time of writing, the latest version of the No-IP DUC was
3.3.0 -
install the DUC OS package
cd /opt/noip-duc/noip-duc_3.3.0/binaries/in this directory there are a few options (files):
noip-duc_3.3.0_amd64.deb
noip-duc_3.3.0_arm64.deb
noip-duc_3.3.0_armhf.deb
noip-duc_3.3.0_x86_64-musl.gzchoose the correct option for the server's platform
in this example, the server is a Raspberry Pi and we will choose
noip-duc_3.3.0_arm64.debsudo apt-get install ./noip-duc_3.3.0_arm64.deb -
setup a systemd service for the DUC
-
create a symlink for the service
sudo ln -s /opt/noip-duc/noip-duc_3.3.0/debian/service /etc/systemd/system/noip-duc.service -
edit or create this file:
/etc/default/noip-ducexample:
sudo nano /etc/default/noip-ducplace this into the file:
## File: /etc/default/noip-duc
NOIP_USERNAME=your-noip-ddns-key-username
NOIP_PASSWORD=your-noip-ddns-key-password
NOIP_HOSTNAMES=your.ddns-hostname.comreplace the values with your appropriate settings
ℹ️ Note: the username and password are not your No-IP credentials, they are a specific DDNS key (username and password pair) that you generate for your DDNS hostname in order for the DUC to authenticate properly, located in your account at: https://my.noip.com/ddns-keys
-
enable the DUC service
sudo systemctl daemon-reload
sudo systemctl enable noip-duc
sudo systemctl start noip-duc
sudo systemctl status noip-duc
-
-
reboot server
sudo reboot
-
Step 3: Setup Firewall
-
ensure that your server is reachable at port
80and443on the firewall in front of your server -
if your server is setup at home then go to your router's port forwarding settings and allow port
80and443traffic to forward to your home server
Step 4: Install & Configure Outline Shadowsocks Server
-
SSH into your server
ℹ️ Note: in this document, the commands listed assume you are using a non-root user on the system that has sudo privileges
-
download and unpack the Outline Shadowsocks server
sudo mkdir -p /opt/outline/bin
cd /opt/outline/bin/
sudo wget https://github.com/OutlineFoundation/tunnel-server/releases/download/v1.9.2/outline-ss-server_1.9.2_linux_arm64.tar.gz
sudo tar xzf outline-ss-server_1.9.2_linux_arm64.tar.gzℹ️ Note: at the time of writing, the latest version of the Outline Shadowsocks server was
1.9.2ℹ️ Note: in the above
wgetcommand, we use the arm64 package because we are on a Raspberry Pi system, replace this URL to the appropriate platform you are using from: https://github.com/OutlineFoundation/tunnel-server/releases -
[optional] add the following script to make managing Outline Shadowsocks clients easier
-
edit create this file:
/opt/outline/bin/outline-ss-clientsexample:
sudo nano /opt/outline/bin/outline-ss-clientsplace this into the file:
-
#!/bin/bash
OUTLINE_HOME="/opt/outline"
OUTLINE_CONFIG_HOME="${OUTLINE_HOME}/config"
OUTLINE_SERVER_CONFIG="${OUTLINE_CONFIG_HOME}/server.yaml"
OUTLINE_CLIENT_CONFIG_HOME="${OUTLINE_CONFIG_HOME}/clients"
OUTLINE_HOSTNAME="$(grep -l outline-ss-server /etc/nginx/sites-enabled/* | xargs grep server_name | head -1 | awk '{print $2}' | tr -d ';')"
KEY_CIPHER="chacha20-ietf-poly1305"
KEY_SECRET_LENGTH=20
function display_usage() {
cat <<EOF
Manage Outline Shadowsocks clients
Usage: $(basename "${0}") <command>
Commands:
list List all clients added
add Add client
remove <client-name> Remove client
qrcode <client-name> Generate QR code for client access key
EOF
}
COMMAND="${1}"
shift
case "${COMMAND}" in
"list"|"add"|"remove"|"qrcode")
# all good
;;
*)
display_usage >&2
exit 1
;;
esac
function list() {
echo "Client List"
echo "-----------"
yq -r '.services[0].keys[].id' ${OUTLINE_SERVER_CONFIG}
return 0
}
function add() {
read -p "Enter a name for the client: " client_id
if [[ ! "${client_id}" =~ ^[a-z][0-9a-z-]+[0-9a-z]$ ]] ; then
echo "Client name must be at least 3 characters in length, and can contain only alphanumeric and hyphen characters"
exit 1
fi
yq --exit-status --yaml-output '.services[0].keys[] | select(.id == "'${client_id}'")' ${OUTLINE_SERVER_CONFIG} > /dev/null 2>&1
if (( ${?} == 0 )) ; then
echo "Client \"${client_id}\" already exists"
exit 1
fi
key_secret="$(cat /dev/urandom | tr -dc 'a-f0-9' | head -c ${KEY_SECRET_LENGTH})"
transport_tcp_endpoint_url="$(yq -r '.services[0].listeners[] | select(.type == "websocket-stream") | .path' ${OUTLINE_SERVER_CONFIG})"
transport_udp_endpoint_url="$(yq -r '.services[0].listeners[] | select(.type == "websocket-packet") | .path' ${OUTLINE_SERVER_CONFIG})"
yq --yaml-output '.services[0].keys +=
[
{
"id": "'${client_id}'",
"cipher": "'${KEY_CIPHER}'",
"secret": "'"${key_secret}"'"
}
]
' ${OUTLINE_SERVER_CONFIG} > ${OUTLINE_SERVER_CONFIG}.tmp
mv ${OUTLINE_SERVER_CONFIG}.tmp ${OUTLINE_SERVER_CONFIG}
echo "Updated server config with new client access key"
cat <<EOF > "${OUTLINE_CLIENT_CONFIG_HOME}/${client_id}.yaml"
---
transport:
\$type: "tcpudp"
tcp:
\$type: "shadowsocks"
endpoint:
\$type: "websocket"
url: "wss://${OUTLINE_HOSTNAME}${transport_tcp_endpoint_url}"
cipher: "chacha20-ietf-poly1305"
secret: "${key_secret}"
udp:
\$type: "shadowsocks"
endpoint:
\$type: "websocket"
url: "wss://${OUTLINE_HOSTNAME}${transport_udp_endpoint_url}"
cipher: "chacha20-ietf-poly1305"
secret: "${key_secret}"
EOF
echo "Created new client config file"
echo "Client config file: ${OUTLINE_CLIENT_CONFIG_HOME}/${client_id}.yaml"
client_config_uri="${OUTLINE_HOSTNAME}/clients/${client_id}.yaml"
client_config_url="https://${client_config_uri}"
client_access_key="ssconf://${client_config_uri}"
echo "Client config URL: ${client_config_url}"
echo "Client access key: ${client_access_key}"
systemctl restart outline-ss-server
echo "Restarted Outline Shadowsocks server"
return 0
}
function remove() {
local client_id="${1}"
read -p "Do you really want to remove client \"${client_id}\"? [Y/n]" confirm_remove
if [[ "${confirm_remove}" != "Y" ]] ; then
echo "Aborting operation"
exit 1
fi
yq --exit-status --yaml-output '.services[0].keys[] | select(.id == "'${client_id}'")' ${OUTLINE_SERVER_CONFIG} > /dev/null 2>&1
if (( ${?} != 0 )) ; then
echo "Client \"${client_id}\" does not exist"
exit 1
fi
yq --yaml-output 'del(.services[0].keys[] | select(.id == "'${client_id}'"))' ${OUTLINE_SERVER_CONFIG} > ${OUTLINE_SERVER_CONFIG}.tmp
mv ${OUTLINE_SERVER_CONFIG}.tmp ${OUTLINE_SERVER_CONFIG}
echo "Updated server config by removing client access key"
echo "Client config file: ${OUTLINE_CLIENT_CONFIG_HOME}/${client_id}.yaml"
rm "${OUTLINE_CLIENT_CONFIG_HOME}/${client_id}.yaml" > /dev/null 2>&1 || true
echo "Removed client config file"
systemctl restart outline-ss-server
echo "Restarted Outline Shadowsocks server"
return 0
}
function qrcode() {
if ! which qrencode > /dev/null ; then
apt-get install -y qrencode > /dev/null
fi
local client_id="${1}"
yq --exit-status --yaml-output '.services[0].keys[] | select(.id == "'${client_id}'")' ${OUTLINE_SERVER_CONFIG} > /dev/null 2>&1
if (( ${?} != 0 )) ; then
echo "Client \"${client_id}\" does not exist"
exit 1
fi
client_config_uri="${OUTLINE_HOSTNAME}/clients/${client_id}.yaml"
client_access_key="ssconf://${client_config_uri}"
qrencode -t "ANSIUTF8" "${client_access_key}"
return 0
}
case "${COMMAND}" in
"add"|"list")
if (( ${#} != 0 )) ; then
display_usage >&2
exit 1
fi
"${COMMAND}"
;;
"remove"|"qrcode")
if (( ${#} != 1 )) ; then
display_usage >&2
exit 1
fi
"${COMMAND}" "${1}"
;;
*)
display_usage >&2
exit 1
;;
esac
-
configure Outline Shadowsocks server
sudo mkdir -p /opt/outline/config-
create this file:
/opt/outline/config/server.yamlexample:
sudo nano /opt/outline/config/server.yamlplace this into the file:
---
web:
servers:
- id: "outline-ss-server"
listen:
- "127.0.0.1:4444"
services:
- listeners:
- type: "websocket-stream"
web_server: "outline-ss-server"
path: "/9d4cd779f1c7c31867dc/tcp"
- type: "websocket-packet"
web_server: "outline-ss-server"
path: "/9d4cd779f1c7c31867dc/udp"
keys:
- id: "client-1"
cipher: "chacha20-ietf-poly1305"
secret: "1fda6bc8e698ade456e9"ℹ️ Note: in the above YAML, the following attributes are important to understand:
web.servers[0].listen[0]- port is set to4444but can be changed to whatever you desireservices[0].listeners[*].path- these paths are secret, change them to anything that is long and difficult to guess, keep it secretservices[0].keys[0].secret- this is the secret for your first initial client, change this to anything that is 20 characters long, keep it secret
-
-
configure initial Outline Shadowsocks client
sudo mkdir -p /opt/outline/config/clients-
create this file:
/opt/outline/config/clients/client-1.yamlexample:
sudo nano /opt/outline/config/clients/client-1.yamlplace this into the file:
---
transport:
$type: "tcpudp"
tcp:
$type: "shadowsocks"
endpoint:
$type: "websocket"
url: "wss://fake-website.com/9d4cd779f1c7c31867dc/tcp"
cipher: "chacha20-ietf-poly1305"
secret: "1fda6bc8e698ade456e9"
udp:
$type: "shadowsocks"
endpoint:
$type: "websocket"
url: "wss://fake-website.com/9d4cd779f1c7c31867dc/udp"
cipher: "chacha20-ietf-poly1305"
secret: "1fda6bc8e698ade456e9"ℹ️ Note: in the above YAML, the following attributes are important to understand:
transport.tcp.endpoint.url- the path in this URL, afterfake-website.commust match the server config (above) listener of type "websocket-stream"transport.udp.endpoint.url- the path in this URL, afterfake-website.commust match the server config (above) listener of type "websocket-packet"transport.tcp.secret- this secret must match the server config (above) key with id "client-1"transport.udp.secret- this secret must match the server config (above) key with id "client-1"
-
ℹ️ Note: replace
fake-website.comin all of the above with the domain that you purchased
- setup a systemd service for the Outline Shadowsocks server
sudo mkdir -p /opt/outline/debian-
create this file:
/opt/outline/debian/outline-ss-server.serviceexample:
sudo nano /opt/outline/debian/outline-ss-server.serviceplace this into the file:
[Unit]
Description=Outline Shadowsocks Server
After=network.target auditd.service
[Service]
ExecStart=/opt/outline/bin/outline-ss-server --config /opt/outline/config/server.yaml --replay_history 10000
Restart=on-failure
Type=simple
[Install]
WantedBy=multi-user.target -
create a symlink for the service
sudo ln -s /opt/outline/debian/outline-ss-server.service /etc/systemd/system/outline-ss-server.service -
enable the Outline Shadowsocks server service
sudo systemctl daemon-reload
sudo systemctl enable outline-ss-server
sudo systemctl start outline-ss-server
sudo systemctl status outline-ss-server
-
Step 5: Setup Bogus (Fake) Website
-
SSH into your server
ℹ️ Note: in this document, the commands listed assume you are using a non-root user on the system that has sudo privileges
-
nginx was installed, previously, in the Step 2.1: Install OS Packages section
-
create some fake website content
ℹ️ Note: for simplicity we are creating a very basic "Hello, World!" index file but it is better to create a proper-looking, albeit fake, website
- add fake website content
sudo mkdir -p /var/www/fake-website.com-
edit or create this file:
/var/www/fake-website.com/index.htmlexample:
sudo nano /var/www/fake-website.com/index.htmlplace this into the file:
Hello, World!
/var/www/fake-website.com -
- add fake website content
-
configure the website to be served by nginx
-
create this file:
/etc/nginx/sites-available/fake-website.com.confexample:
sudo nano /etc/nginx/sites-available/fake-website.com.confplace this into the file:
server {
server_name fake-website.com;
listen 80;
listen [::]:80;
root /var/www/fake-website.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
-
-
enable the newly created website in nginx
cd /etc/nginx/sites-enabled/
sudo ln -s ../sites-available/fake-website.com.conf fake-website.com.conf -
get a free HTTPs (TLS/SSL) certificate via certbot
- certbot was installed, previously, in the Step 2.1: Install OS Packages section
- run certbot for nginx to get a certificate that is automatically renewed
follow the instructions in the terminal to complete the certificate request
sudo certbot --nginx --agree-tos - verify the certbot auto-renewal service is running
sudo systemctl status certbot.timer
-
finalize the website configuration in nginx
-
create this file:
/etc/nginx/sites-available/fake-website.com.confexample:
sudo nano /etc/nginx/sites-available/fake-website.com.confplace this into the file:
upstream outline-ss-server {
# this port must match Outline Shadowsocks server config
server localhost:4444;
}
server {
server_name fake-website.com;
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/fake-website.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fake-website.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/fake-website.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# this path must match Outline Shadowsocks server config
# for listener of type "websocket-stream"
location /9d4cd779f1c7c31867dc/tcp {
proxy_pass http://outline-ss-server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# this path must match Outline Shadowsocks server config
# for listener of type "websocket-packet"
location /9d4cd779f1c7c31867dc/udp {
proxy_pass http://outline-ss-server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
server_name fake-website.com;
listen 80;
listen [::]:80;
if ($host = fake-website.com) {
return 301 https://$host$request_uri;
}
return 404;
}ℹ️ Note: if you changed the Outline Shadowsock server port to something other than
4444then make sure to update the port in this config file (see commented sections)ℹ️ Note: if you changed the Outline Shadowsock server listeners paths to something new then make sure it matches in this config file (see commented sections)
-
-
create a symlink for the Outline client configs
sudo ln -s /opt/outline/config/clients /var/www/fake-website.com/clients -
reboot server
sudo reboot- after the server finishes rebooting, ensure all services are running correctly
sudo systemctl status
- after the server finishes rebooting, ensure all services are running correctly
-
verify your website is reachable
- in a browser, go to: http://fake-website.com/
- the browser should have been redirected from HTTP to HTTPs: https://fake-website.com/
- verify that you can see your website
- if you used the simple "Hello, World!" setup then that is what you should see in the browser
- in a browser, go to: http://fake-website.com/
ℹ️ Note: replace
fake-website.comin all of the above with the domain that you purchased
Step 6: Manage Outline Clients
- in general, when you open an Outline client app, it asks you for an access key
-
access keys must be entered in this format:
ssconf://fake-website.com/clients/client-id.yamlℹ️ Note: replace
client-idwith correct client idif you want to avoid using this tedious URL then you can use a QR code instead, using the Outline Client manager script (below)
-
ℹ️ Note: replace
fake-website.comin all of the above with the domain that you purchased
Step 6.1: [optional] Using Client Manager Script
-
SSH into your machine
ℹ️ Note: in this document, the commands listed assume you are using a non-root user on the system that has sudo privileges
-
if you installed the optional client management script from Step 4: Install & Configure Outline Shadowsocks Server then you can use it to manage your clients as follows:
List existing clients
cd /opt/outline/bin/
sudo ./outline-ss-clients list
Add new client
cd /opt/outline/bin/
sudo ./outline-ss-clients add
Display QR code for existing client
cd /opt/outline/bin/
sudo ./outline-ss-clients qrcode client-id
ℹ️ Note: replace
client-idwith correct client id
a QR code will be generated in the terminal, and you can scan this with a mobile device that has the Outline client app installed by opening the camera app and pointing it at the QR code
example QR code generated for ssconf://fake-website.com/clients/client-1.yaml:
█████████████████████████████████████
█████████████████████████████████████
████ ▄▄▄▄▄ █▀ █▀▀▄█▀▄▀ ▄▄█ ▄▄▄▄▄ ████
████ █ █ █▀ ▄ █▀ ▄▄▀█ ▄█ █ █ ████
████ █▄▄▄█ █▀█ █▄█▀▀ ▄▄▀█ █▄▄▄█ ████
████▄▄▄▄▄▄▄█▄█▄█ █ █▄█▄█ █▄▄▄▄▄▄▄████
████ ▄ ▀▄ ▄█▄ ▄▄▀ ▀▀▀ ▀ ▀ ▀ ▀████
████▄ █▄▀ ▄█ ▀ ▄▄▄▄ ███▀██▄█ ▄█▀████
████▄▀▀▄██▄██▄▀▄ ▄ ██ ▀ ▀▀▀▀ ▀████
█████▀▀▄ ▄▄ ▀█▄█▄▄▄▄██▄█▄▄ ▀▄█▀████
████ █ █▄ █ ▄ █▄ ▄▀ ▀█▀▀▀▀▄▄▀████
████ █▀█▄█▄█▄█ ███ ▀▄▄▀▄█▀▀▀█▄█▀████
████▄██▄▄▄▄█ ▀ ▄▄▀▄ ▀▀ ▄▄▄ ▄ ▄▀████
████ ▄▄▄▄▄ █▄▀▀█▀█▀▄▀█▀ █▄█ █▄█▀████
████ █ █ █ ▄ ▀▄ ██▀ ▀█ ▄▄▄▄▀ ▀ ████
████ █▄▄▄█ █ ▄ █▀▄▄ █▄█▀▄▄ ▄▄▄ █████
████▄▄▄▄▄▄▄█▄██▄█▄▄▄█▄█▄███▄██▄██████
█████████████████████████████████████
█████████████████████████████████████
Remove existing client
cd /opt/outline/bin/
sudo ./outline-ss-clients remove client-id
ℹ️ Note: replace
client-idwith correct client id
Step 7: [optional] Configure Scheduled Server Restart
-
SSH into your machine
ℹ️ Note: in this document, the commands listed assume you are using a non-root user on the system that has sudo privileges
-
it is good practice to reboot your server at least once a week to give it refresh
-
setup a scheduled task in crontab
crontab -eplace this into the file:
# m h dom mon dow command
0 2 * * 0 sudo rebootin this cron schedule the server will reboot every Sunday at 02:00