Nginx in front of VLESS XHTTP
In this article, Nginx sits in front of Xray, which implements the VLESS protocol with XHTTP transport. Transport Layer Security (TLS) is processed by Nginx before traffic reaches Xray. This makes your proxy server resemble a realistic web server. The Nginx and Xray configurations are almost entirely copied from lxhao61/integrated-examples. This article fleshes out the practical details of how to install and configure the software.
1. Set up server
1.1. Create DNS records
You will need to purchase a domain name to implement the configuration in this article.
The configurations in this article allow you to reach the server either directly or through a content distribution network (CDN). This will require two subdomains, both mapped to your server's IP address:
- One subdomain (given in the examples as
h3.xjkj8.xyz
) is not proxied, but reaches the server direct from the client over HTTP/3. - A second subdomain (given in the examples as
z1.xjkj8.xyz
) is proxied through a CDN.
Depending on your requirements, you can either follow the example exactly and support both options, or eliminate the one you do not need.
1.2. Open firewall
Open your server's firewall and/or security groups for input:
tcp/80
for HTTP input and SSL hostname validationtcp/443
for HTTPS inputudp/443
for HTTP/3 input
1.3. Install Xray
SSH into your server as root
. On Windows, you can do this with Windows PowerShell. On macOS or Linux, you can use your built-in terminal emulator.
Update the server:
apt update && apt upgrade -y && apt autoremove -y
As explained in XTLS/Xray-install, install Xray by running the script:
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
1.4. Choose parameters
Generate a universally unique id (UUID) to serve as a password for VLESS:
xray uuid
Example:
8104b4b2-976c-4e53-97db-f654fc8620fb
Generate a secret path for reaching Xray through Nginx:
< /dev/urandom tr -dc a-z0-9 | head -c${1:-8};echo;
Example:
4qgjtkw3
1.5. Configure Xray
Edit the Xray confuration file:
vi /usr/local/etc/xray/config.json
Use the model below, replacing the sample values by your own values. Xray will listen only on localhost to prevent unauthorized visitors from detecting its presence. Port 127.0.0.1:2023
was chosen as an example. You can change the port number if you wish, as long as you are consistent between Xray and Nginx. Change the sample UUID and path in the example to your actual chosen UUID and path.
{ "log": { "loglevel": "warning", "error": "/var/log/xray/error.log", "access": "/var/log/xray/access.log" }, "inbounds": [ { "listen": "127.0.0.1", "port": 2023, "protocol": "vless", "settings": { "clients": [ { "id": "8104b4b2-976c-4e53-97db-f654fc8620fb" } ], "decryption": "none" }, "streamSettings": { "network": "xhttp", "xhttpSettings": { "path": "/4qgjtkw3" } }, "sniffing": { "enabled": true, "destOverride": [ "http", "tls", "quic" ] } } ], "routing": { "rules": [ { "type": "field", "protocol": [ "bittorrent" ], "outboundTag": "block" } ] }, "outbounds": [ { "protocol": "freedom", "settings": {} }, { "tag": "block", "protocol": "blackhole", "settings": {} } ] }
Save the file with your changes in it.
1.6. Start Xray
Restart Xray with your new configuration:
systemctl restart xray
Check that Xray is active (running)
:
systemctl status xray
1.7. Install Nginx
The easiest way to install Nginx is to use your Linux distribution's repositories. You can certainly do that. However, the version of Nginx in your distribution's repositories may be quite old. Therefore what follows are instructions for installing Nginx direct from the Nginx project's repositories. This will give you an up-to-date version. The instructions here are a copy of what appears on nginx.com.
First install the prerequisites:
apt install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring
Download the official Nginx signing key:
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
Display the codename of your distribution:
lsb_release -cs
Example:
bookworm
Create a file /etc/apt/sources.list.d/nginx.list
:
vi /etc/apt/sources.list.d/nginx.list
Insert an entry:
deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian bookworm nginx
Save the file:
Set up repository pinning to prefer the Nginx project's packages over your distribution's ones. Create a file /etc/apt/preferences.d/99nginx
:
vi /etc/apt/preferences.d/99nginx
Insert these contents (with a blank line at the end):
Package: * Pin: origin nginx.org Pin: release o=nginx Pin-Priority: 900
Save the file.
Update your package metadata for the new repository:
apt update
Install Nginx:
apt install nginx -y
Check which version of Nginx you have:
nginx -v
Example:
nginx version: nginx/1.26.2
Enable, start, and check the status of Nginx:
systemctl enable nginx
systemctl start nginx
systemctl status nginx
1.8. Obtain SSL certificate
Install the snap daemon:
apt install -y snapd
Install the certbot
snap:
snap install --classic certbot
Create a symbolic link to the certbot
binary:
ln -s /snap/bin/certbot /usr/bin/certbot
Invoke certbot
. You need certonly
because you are going to configure Nginx yourself. You need --nginx
so that certbot
can share port tcp/80
with Nginx. Of course, you must specify your own domains in place of the examples given here. Each subdomain must be requested separately.
certbot certonly --nginx --agree-tos --register-unsafely-without-email -d h3.xjkj8.xyz
The certificate is saved at /etc/letsencrypt/live/h3.xjkj8.xyz/fullchain.pem
. The key is saved at /etc/letsencrypt/live/h3.xjkj8.xyz/privkey.pem
.
certbot certonly --nginx --agree-tos --register-unsafely-without-email -d z1.xjkj8.xyz
The certificate is saved at /etc/letsencrypt/live/z1.xjkj8.xyz/fullchain.pem
. The key is saved at /etc/letsencrypt/live/z1.xjkj8.xyz/privkey.pem
.
Test certificate renewal with a dry run:
certbot renew --dry-run
1.9. Configure Nginx
Edit the Nginx configuration file:
vi /etc/nginx/nginx.conf
Completely delete the existing contents.
Insert your own contents, using the following as a model. Go through the model carefully, line by line, changing it to fit your environment and your needs.
# The commented out user implies Nginx will run as the default user (root). # If you uncomment and edit the next line, # make sure the user and group have the correct permissions. #user nobody nogroup; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # Use the map function to let the custom variable $clientRealIp get the client's real IP address. # The configuration is as follows (this is optional): map $http_x_forwarded_for $clientRealIp { "" $remote_addr; "~*(?P<firstAddr>([0-9a-f]{0,4}:){1,7}[0-9a-f]{1,4}|([0-9]{1,3}\.){3}[0-9]{1,3})$" $firstAddr; } # Redefine the access log format. # The configuration is as follows (this is optional): log_format main '$clientRealIp $remote_addr $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" $http_x_forwarded_for ' '"$upstream_addr" "$upstream_status" "$upstream_response_time" "$request_time" '; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; server { listen 80; # If your server has no IPv6, the next line can be deleted. listen [::]:80; # Automatically redirect HTTP to HTTPS, to make the website look more authentic. return 301 https://$host$request_uri; } server { # Listen statements for where the version is not less than v1.25.1. # Otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 ssl default_server; listen [::]:443 ssl default_server; http2 on; # Listen statements for where the version is is less than v1.25.1. # Otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 ssl http2 default_server; listen [::]:443 ssl http2 default_server; ssl_protocols TLSv1.2 TLSv1.3; # The next line is supported only for versions no less than v1.19.4. # It limits domain name connections # (including prohibiting access to websites via IP). ssl_reject_handshake on; } # If the certificates in the server block above # and the certificates in the server block below # use the same wildcard certificate # or Subject Alternative Name (SAN) certificate, # the next part of the configuration can be deleted # and the related configuration details merged. server { # Listen statements for where the version is not less than v1.25.1. # Otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 ssl; listen [::]:443 ssl; http2 on; # Listen statements for where the version is is less than v1.25.1. # Otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 ssl http2; listen [::]:443 ssl http2; # Change the server name to your own domain name server_name z1.xjkj8.xyz; # Replace with your own certificate's absolute path. ssl_certificate /etc/letsencrypt/live/z1.xjkj8.xyz/fullchain.pem; # Replace with your own key's absolute path. ssl_certificate_key /etc/letsencrypt/live/z1.xjkj8.xyz/privkey.pem; # If using the OpenSSL library, TLSv1.3 requires # the OpenSSL library version to be no less than 1.1.1 to be supported. ssl_protocols TLSv1.2 TLSv1.3; # Prioritize the server's cipher suite # (valid for the following TLSv1.2 protocol cipher suites). ssl_prefer_server_ciphers on; # If the certificate is an RSA certificate, change all ECDSA to RSA. ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305; # If using the OpenSSL library, this configuration parameter # requires the OpenSSL library version to be # no less than 3.0.0 to be supported. ssl_ecdh_curve secp521r1:secp384r1:secp256r1:x25519; # The location path corresponds to the path # in the VLESS XHTTP application location /4qgjtkw3 { proxy_http_version 1.1; # Forward to the local VLESS + XHTTP listening port proxy_pass http://127.0.0.1:2023; proxy_redirect off; proxy_request_buffering off; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { # Enable HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Modify the web root file path to your own location, if necessary root /var/www/html; index index.html index.htm; } } server { # Listen statements for where the version is not less than v1.25.0 # and the SSL library supports QUIC (HTTP/3) configuration, # otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 quic reuseport; listen [::]:443 quic reuseport; # Listen statements for where the version is not less than v1.25.1. # Otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 ssl; listen [::]:443 ssl; http2 on; # Listen statements for where the version is less than v1.25.1. # Otherwise they must be deleted. # You can delete the IPv6 line if your server has no IPv6. listen 443 ssl http2; listen [::]:443 ssl http2; # Change to your own domain name server_name h3.xjkj8.xyz; # Replace with your own certificate's absolute path. ssl_certificate /etc/letsencrypt/live/h3.xjkj8.xyz/fullchain.pem; # Replace with your own key's absolute path. ssl_certificate_key /etc/letsencrypt/live/h3.xjkj8.xyz/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; # Prioritize the server's cipher suite # (valid for the following TLSv1.2 protocol cipher suites) ssl_prefer_server_ciphers on; # If the certificate is an RSA certificate, change all ECDSA to RSA. ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305; # If using the OpenSSL library, this configuration parameter # requires the OpenSSL library version to be # no less than 3.0.0 to be supported. ssl_ecdh_curve secp521r1:secp384r1:secp256r1:x25519; # The location path corresponds to the path # in the VLESS XHTTP application location /4qgjtkw3 { proxy_http_version 1.1; # Forward to the local VLESS XHTTP listening port proxy_pass http://127.0.0.1:2023; proxy_redirect off; proxy_request_buffering off; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { # Enable HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Announce the availability of HTTP/3 server add_header Alt-Svc 'h3=":443"; ma=86400'; # Modify the web root file path to your own location, if necessary root /var/www/html; index index.html index.htm; } } }
Save the file once you are done making changes.
1.10. Restart Nginx
Check the validity of your Nginx configuration file:
nginx -t
Restart Nginx with your new configuration file:
systemctl restart nginx
Check Nginx status:
systemctl status nginx
1.11. Add camouflage web content
Add some web content to /var/www/html
. In the example given here, we will use PavelDoGreat/WebGL-Fluid-Simulation:
curl -L https://github.com/PavelDoGreat/WebGL-Fluid-Simulation/archive/refs/heads/master.zip -O
Extract the archive:
apt install -y unzip
unzip master.zip
Copy the web content into place:
mkdir -p /var/www/html
cp -r WebGL-Fluid-Simulation-master/* /var/www/html
ls -l /var/www/html
You can now exit your SSH session with the server:
exit
1.12. Check HTTP/3 support
Open a browser on your workstation. Attempt to visit https://h3.xjkj8.xyz
.
If you can reach your domain, go to https://www.http3check.net.
Check your domain https://h3.xjkj8.xyz
. Verify that QUIC is supported and HTTP/3 is supported.
2. Set up client
Use either the command-line client or the graphical user interface (GUI) client, as you prefer. We will give instructions for the h3
subdomain.
2.1. Command-line client
Download the CLI client from https://github.com/XTLS/Xray-core/releases. On Windows that will be Xray-windows-64.zip
. Unzip the .zip
file.
Create a config.json
configuration file in the same folder as the app (e.g. Downloads\Xray-windows-64
). Use the following as a model. Change the server, path, and UUID to your own values.
{ "inbounds": [ { "sniffing": { "enabled": true, "destOverride": [ "http", "tls", "quic" ] }, "port": 10808, "listen": "127.0.0.1", "protocol": "socks", "settings": { "udp": true } } ], "outbounds": [ { "protocol": "vless", "settings": { "vnext": [ { "address": "h3.xjkj8.xyz", "port": 443, "users": [ { "id": "8104b4b2-976c-4e53-97db-f654fc8620fb", "encryption": "none" } ] } ] }, "streamSettings": { "network": "xhttp", "security": "tls", "xhttpSettings": { "path": "/4qgjtkw3", "host": "h3.xjkj8.xyz" }, "tlsSettings": { "serverName": "h3.xjkj8.xyz", "alpn": [ "h3" ] } } } ] }
Save the config.json
file.
Open a command prompt window.
Run Xray from the command line:
xray.exe -c config.json
Configure your browser or your system-wide proxy to use the SOCKS5 proxy at 127.0.0.1
port 10808
.
Check that you can surf the web.
2.2. GUI client
The latest version of v2rayN supports xhttp.
Download the latest version of v2rayN-windows-64-With-Core.zip
from https://github.com/2dust/v2rayN.
Unzip the .zip
file
Configure v2rayN to use the HTTP/3 subdomain (in this example).
For greater realism, add a browser fingerprint.
Connect your client to your server in v2rayN.
Configure your browser or your system-wide proxy to use the SOCKS5 proxy at 127.0.0.1
port 10808
.
Check that you can surf the web.