Everything described below is the result of a technical experiment. The material is not advertising, does not call for any action, is provided solely for informational purposes, and was prepared as part of research.
Main information source: GitHub repository
VPS Server
A VPS can be bought almost anywhere, with the ability to pay with almost anything, including crypto. Working examples: timeweb.cloud, hostmenow.org, regxa.com. As a result, you should have a login and password for a Debian server with a public IP address. As an example, we will use 10.10.10.10.
Domain
Buy a domain name. The easiest option is to do it in the same place where the VPS was purchased. If you want to save money, you can go to spaceship.com, or you can buy one at reg.ru. In general, there are many options, and the price will depend only on what you choose. As an example: example-site.com.
DNS
In the same place where you bought the domain, add the public address to DNS records so that the whole world knows its heroes. It will look approximately like this:
- record 1
- Type - A (IPv4 address)
- Name - example-site.com
- Value - 10.10.10.10
- TTL - choose the minimum value, for example “1 hour”
- record 2
- Type - A (IPv4 address)
- Name - www.example-site.com
- Value - 10.10.10.10
- TTL - choose the minimum value, for example “1 hour”
Some providers expect different values in the Name fields:
- instead of example-site.com - @
- instead of www.example-site.com - www
After everything is configured, you need to wait… To check DNS record propagation periodically, you can run:
sudo dig example-site.com
The ANSWER field should be NOT 0.
nginx
While records are propagating, nginx can be installed and configured:
Update repositories and install the package:
sudo apt update && sudo apt install nginx -y
Create a working folder for the site:
sudo mkdir -p /var/www/example-site
sudo chown -R $USER:$USER /var/www/example-site
Create a test page:
sudo echo "<h1>[ THE EXAMPLE SITE: OPERATIONAL ]</h1>" > /var/www/example-site/index.html
Create the configuration file:
sudo mcedit /etc/nginx/sites-available/example-site
and add this to it:
server {
listen 80;
server_name example-site.com www.example-site.com;
root /var/www/example-site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Create a symbolic link and enable it:
sudo ln -s /etc/nginx/sites-available/example-site /etc/nginx/sites-enabled
Remove the default config:
sudo rm /etc/nginx/sites-enabled/default
sudo rm /etc/nginx/sites-available/default
Check the system for errors and restart:
sudo nginx -t
sudo systemctl restart nginx
The last point is the “golden nginx rule”: created/changed, checked, restarted
It is recommended to make the site page look like a working site. You can ask any neural network to generate it.
SSL
When the ANSWER field becomes not 0, get a certificate. To do this, install the Let’s Encrypt bot, which will take care of it.
sudo apt update && sudo apt install certbot python3-certbot-nginx -y
Then get the certificate (if the ANSWER field is 0, the certificate cannot be obtained):
sudo certbot --nginx -d example-site.com -d www.example-site.com
- The bot will ask for an email for renewal notifications.
- It will ask you to agree to the terms.
- It will ask whether to make a Redirect (redirect from http to https) - choose YES.
- The bot will rewrite the Nginx config and add protection.
Let’s Encrypt certificates live for 90 days. Certbot creates a scheduled task for auto-renewal; you can check it with:
sudo certbot renew --dry-run
As a result, when opening “example-site.com” from the local machine, the connection should go through https and no one should complain.
Preparing nginx
For both the site and Xray to live on port 443, we need to implement fallback: a rollback to the site request. Xray will become the main service: it first accepts incoming connections on port 443 and does the following:
if a VPN client knocks with the correct key, Xray lets it through;
if a regular user or scanner bot comes in, Xray forwards the request to the local site.
Since the site is currently listening on port 443, it must be moved to another port (for example, 8443), which will be available only inside the server (localhost). To do this, edit the configuration that the Let’s Encrypt bot edited:
sudo mcedit /etc/nginx/sites-available/example-site
and change:
server {
server_name example-site.com www.example-site.com;
root /var/www/example-site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# BEFORE: listen 443 ssl; # managed by Certbot
# AFTER:
listen 127.0.0.1:8443 ssl;
ssl_certificate /etc/letsencrypt/live/example-site.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example-site.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.example-site.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = example-site.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name example-site.come www.example-site.com;
return 404; # managed by Certbot
}
After saving, check and restart:
sudo nginx -t && sudo systemctl restart nginx
It is worth noting an important security point: Make sure that port 8443 is closed externally and available only to 127.0.0.1. You can check this with:
sudo ss -tulpn
The output should contain:
127.0.0.1:8443
Port 8443 should not be “hanging” anywhere else.
Xray
Installing Xray from the Official Script:
sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
The command downloads the latest Xray version and creates a system service.
Generating Keys and UUID
For the Reality protocol to work, keys must be generated:
sudo xray x25519
After running the command, you receive Private key, Password, and Hash32. They must be saved in a note. The private key goes into the server config, and the password goes into the client settings.
Also generate and save a UUID (password):
sudo xray uuid
and a short server name (shortId), which should also be saved:
sudo openssl rand -hex 8
Configuration Setup
When all required data is available, edit the Xray server configuration file:
sudo mcedit /usr/local/etc/xray/config.json
and write the following into it:
{
"log": {
"loglevel": "warning",
"access": "/var/log/xray/access.log",
"error": "/var/log/xray/error.log"
},
"inbounds": [
{
"listen": "IP_ADDRESS",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "UUID",
"flow": "xtls-rprx-vision",
"level": 0,
"email": "SOME-NAME"
}
],
"decryption": "none",
"fallbacks": [
{
"dest": "127.0.0.1:8443"
}
]
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "127.0.0.1:8443",
"xver": 0,
"serverNames": [
"example-site.com",
"www.example-site.com"
],
"privateKey": "PRIVATE_KEY",
"shortIds": [
"SHORTID"
]
}
}
}
],
"outbounds": [
{
"protocol": "freedom"
},
{
"tag": "block",
"protocol": "blackhole"
}
]
}
Replace “PRIVATE_KEY”, “UUID”, and “SHORTID” with your previously obtained values, and put the public IP address into the “IP_ADDRESS” field (this field can be removed; then Xray will listen on all interfaces). “SOME-NAME” is any name for later user identification.
After updating the configuration, check that everything works:
sudo xray -test -config /usr/local/etc/xray/config.json
If there is a “Configuration OK” message, restart. If not, you can put the config into some autoformatter, for example here.
sudo systemctl restart xray && sudo systemctl status xray
Also check that everything is fine with ports: Xray should occupy port 443.
sudo ss -tulpn
After that, the site should be available again.
BBR
Bottleneck Bandwidth and Round-trip propagation time (BBR) is a congestion-control algorithm for TCP.
Edit repositories:
sudo mcedit /etc/apt/sources.list
Add the following line to the end of the file and save it:
deb http://archive.debian.org/debian buster-backports main
Update the list of available packages and install the latest version:
sudo apt update && sudo apt -t buster-backports install linux-image-amd64
Edit the sysctl.conf configuration file and enable BBR:
sudo mcedit /etc/sysctl.conf
Add the following lines to the end of the file:
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
Reboot the VPS:
sudo reboot
To check BBR operation:
lsmod | grep bbr && lsmod | grep fq
You should see something like:
tcp_bbr 21450 90
sch_fq 21450 2
iPhone, Android, MacOS, Windows
To add another user, add a new object to the “clients” array inside the existing config. Each client must have its own unique UUID. To do this, generate a new UUID and add it to the corresponding field. Xray will understand exactly who connected by the UUID value in the packet header.
Link Generation
The link can be created manually, or not manually… Create a script file:
sudo mcedit ./link_former.py
and insert the code:
#! /bin/python3
import json
import uuid
import subprocess
import urllib.parse
def restart_xray():
try:
subprocess.run(["systemctl", "restart", "xray"], check=True)
print("Xray service restarted successfully.")
except subprocess.CalledProcessError:
print("Error restarting Xray. Check permissions (sudo).")
def add_or_update_user():
config_path = '/usr/local/etc/xray/config.json'
username = input("Enter username: ")
public_key = input("Enter Password: ")
try:
with open(config_path, 'r+') as f:
config = json.load(f)
inbound = config['inbounds'][0]
clients = inbound['settings']['clients']
# Search and overwrite
existing_user = next((c for c in clients if c.get('email') == username), None)
user_id = str(uuid.uuid4())
if existing_user:
existing_user['id'] = user_id
print(f"\nUser '{username}' updated.")
else:
clients.append({"id": user_id, "flow": "xtls-rprx-vision", "email": username, "level": 0})
print(f"\nUser '{username}' added.")
f.seek(0)
json.dump(config, f, indent=2)
f.truncate()
# Parameters from the config
stream_settings = inbound['streamSettings']
reality = stream_settings['realitySettings']
# Build query parameters
params = {
"encryption": "none",
"flow": "xtls-rprx-vision",
"security": "reality",
"sni": reality['serverNames'][0],
"fp": "chrome",
"pbk": public_key,
"sid": reality['shortIds'][0],
"type": "raw"
}
query_string = urllib.parse.urlencode(params)
link = f"vless://{user_id}@{reality['serverNames'][0]}:{inbound['port']}?{query_string}#{username}"
print(f"\nReady link:\n{link}\n")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
add_or_update_user()
restart_xray()
Set execute permissions:
sudo chmod 755 ./link_former.py
After launch:
./link_former.py
The script will ask you to enter:
- a new username - enter whatever you like
- the password received during key generation
As a result, a link will be generated that will be used to configure the client.
v2Box (Android, iPhone, MacOS)
For mobile iPhones and MacOS computers, the v2Box application is used; for Android, v2rayNG. These applications forward all traffic to your Xray server.
To connect, you need to:
- install v2Box;
- copy the generated configuration link;
- go to “Configurations” in the application;
- press plus (+) and select “Import v2ray URI from clipboard”.
v2rayN (Windows)
For Windows, there are different GUI options; one of the most popular is v2rayN:
- go to the official repository;
- download the release for the required architecture and unpack it;
- run the executable file “v2rayN.exe” as administrator;
- go to “Configuration”;
- choose “Import Share link from clipboard”;
- confirm with “Confirm”;
- in the lower part of the program window, switch “Enable Tun” on;
- in the “System proxy” window (to the right of Enable Tun), choose “Set system proxy”;
- in the “Routing” window (to the right of System proxy), choose “V4-Global”.
Linux (CLI)
First, a configuration must be generated, and then Xray clients can be used based on it. The client settings file can be created manually, or not manually:
sudo mcedit ./json_former.py
and insert the code:
#! /bin/python3
import json
import uuid
import subprocess
def restart_xray():
try:
subprocess.run(["systemctl", "restart", "xray"], check=True)
print("Xray service restarted successfully.")
except subprocess.CalledProcessError:
print("Error restarting Xray. Check permissions (sudo).")
def add_or_update_user():
config_path = '/usr/local/etc/xray/config.json'
username = input("Enter username: ")
public_key = input("Enter Password: ")
try:
with open(config_path, 'r+') as f:
config = json.load(f)
inbound = config['inbounds'][0] # Get the first inbound
clients = inbound['settings']['clients']
# Search and overwrite
existing_user = next((c for c in clients if c.get('email') == username), None)
user_id = str(uuid.uuid4())
if existing_user:
existing_user['id'] = user_id
print(f"\nUser '{username}' updated.")
else:
clients.append({"id": user_id, "flow": "xtls-rprx-vision", "email": username, "level": 0})
print(f"\nUser '{username}' added.")
f.seek(0)
json.dump(config, f, indent=2, ensure_ascii=False)
f.truncate()
# Parameters from the config
stream_settings = inbound['streamSettings']
reality = stream_settings['realitySettings']
client_config = {
'log': {
'loglevel': 'warning'
},
'dns': {
"hosts": {
reality['serverNames'][0]: inbound['listen']
},
"servers": [
"8.8.8.8",
"1.1.1.1",
"8.8.4.4"
],
},
"inbounds": [
{
"tag": "socks",
"port": 10808,
"listen": "127.0.0.1",
"protocol": "socks",
"sniffing": {
"enabled": True,
"destOverride": ["http", "tls", "quic"],
"routeOnly": False
},
"settings": {
"auth": "noauth",
"udp": True
}
}
],
"outbounds": [
{ "tag": "proxy",
"protocol": "vless",
"settings": {
"address": reality['serverNames'][0],
"port": inbound['port'],
"id": user_id,
"encryption": "none",
"flow": "xtls-rprx-vision",
"level": 0
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"serverName": reality['serverNames'][0],
"fingerprint": "chrome",
"show": False,
"publicKey": public_key,
"shortId": reality['shortIds'][0]
}
}
},
{
"tag": "dns-out",
"protocol": "dns",
"settings": {
"network": "tcp,udp",
"nonIPQuery": "drop"
}
},
{
"tag": "direct",
"protocol": "freedom"
},
{
"tag": "block",
"protocol": "blackhole"
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": ["socks"],
"port": 53,
"network": "tcp,udp",
"outboundTag": "dns-out"
},
{
"type": "field",
"inboundTag": ["socks"],
"outboundTag": "proxy",
"network": "tcp,udp"
}
]
}
}
client_filename = f"{username}_client.json"
with open(client_filename, 'w') as cf:
json.dump(client_config, cf, indent=2, ensure_ascii=False)
print(f"\nConfiguration saved to file: {client_filename}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
add_or_update_user()
restart_xray()
Set execute permissions:
sudo chmod 755 ./json_former.py
And run it. The idea is the same as in “link_former.py”.
sudo ./json_former.py
There are GUI versions for Linux too, for example the same v2rayN, but for the console version the simplest way is to use the Xray-core itself.
Install Xray with the official script:
sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
Check the configuration and Xray itself:
sudo xray -test -config NEW_CLIENT_CONFIG.json
To forward all traffic through a SOCKS tunnel, it first needs to be wrapped onto a virtual interface. This can be done with tun2proxy. To do this, go to the official repository and copy the link for the required architecture. As an example, take x86.
sudo wget https://github.com/tun2proxy/tun2proxy/releases/latest/download/tun2proxy-x86_64-unknown-linux-gnu.zip
Unpack and set permissions:
sudo unzip ./tun2proxy-x86_64-unknown-linux-gnu.zip && chmod +x tun2proxy-bin
Move it and check that everything is ready:
sudo mv tun2proxy-bin /usr/local/bin/tun2proxy && tun2proxy --version
Then run Xray:
sudo xray run -c client_config.json
and tun2proxy in a neighboring window:
sudo tun2proxy --setup --proxy socks5://127.0.0.1:10808
Replace [IP_ADDRESS] with the public IP of the server.
It should be noted that this trick can be done another way, not through SOCKS but through a transparent proxy (tproxy). But then you must not forget about routing, preferably nftables, and in some places iptables. So, SOCKS…
Route and Blocking Management
To manage traffic more flexibly (for example, send some traffic through Vray and some directly through the provider), you can edit the configuration and add additional rules in the “routing” block.
To block ads, analytics, and prevent work with vulnerable protocols, blocks are used that send all traffic by the “block” tag from the configuration, thereby blocking it:
{
"_note": "Vulnerable prots",
"type": "field",
"inboundTag": ["socks-in"],
"outboundTag": "block",
"network": "udp",
"port": "135,137,138,139"
},
{
"_note": "Adds blocking",
"type": "field",
"inboundTag": ["socks-in"],
"domain": ["geosite.dat:category-ads-all"],
"outboundTag": "block"
}
“Specific IP address” and “Block ads”. Additional settings must be inserted by the same principle as existing ones. Two approaches should be highlighted:
- Automatic
{
"_note": "Domain names",
"type": "field",
"inboundTag": ["socks-in"],
"domain": ["geosite:private", "geosite:category-ru"],
"outboundTag": "direct"
},
{
"_note": "IP addresses",
"type": "field",
"inboundTag": ["socks-in"],
"ip": ["geoip:private", "geoip:ru"],
"outboundTag": "direct"
}
- geosite:private - a domain-name database from which local domains are taken (localhost, .local, .lan)
- geosite:category-ru - a domain-name database from which popular domains in the .ru, .su, .xn–p1ai zones are taken
- geoip:private - an IP-address database from which private IP ranges are taken (192.168.x.x, 10.x.x.x, 127.0.0.1)
- geoip:ru - an IP-address database containing all IP ranges registered to Russian providers
Thus, when applying these settings, part of the traffic will go without Vray, direct. The rest will be sent through Vray.
- Manual
{
"_note": "Manual proxing",
"type": "field",
"domain": [
"domain:2ip.io",
"domain:youtube.com"
],
"outboundTag": "vless-reality"
},
{
"_note": "Manual directing",
"type": "field",
"domain": [
"domain:yandex.ru",
"domain:vk.ru"
],
"outboundTag": "direct"
}
- direct - send without Vray
- proxy - send through Vray
When applying these settings, part of the traffic will go without Vray, direct. The rest will be sent through Vray, proxy.
You can review the domain list, IP addresses, update the database, and learn about even more flexible settings in the GitHub repository.