Setup X-Ray Server in Debian 12

Before we begin, when we come to any "nano" command, it refers to a text editor in linux. It is recommended to copy them to notepad first, then copy from notepad to putty. As I found that some php code will also be copied to putty if we copy the code directly from here. I will always remind you to copy to notepad first when we come to those instance. When you finished editing the file, press "Ctrl"+"X" together, then press "Y" to confirm edit, follow with "Enter" to quit editor. I will always call that command before I explain what I'm going to do with that file, so you won't be confused for what should copy

1. Setting up environment of Debian

Get super user right

sudo -i

Change timezone to HK

timedatectl set-timezone Asia/Hong_Kong

Edit source list for updating image to Debian 11 (in case there you want to update from old version or there is no Bullseye image in VPS)

nano /etc/apt/sources.list

Change the file to following (Please copy it to notepad first)

deb https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb https://deb.debian.org/debian-security/ bookworm-security main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian-security/ bookworm-security main contrib non-free non-free-firmware
deb https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
deb https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
deb https://debian.mirror.constant.com bookworm main contrib non-free non-free-firmware
deb-src https://debian.mirror.constant.com bookworm main contrib non-free non-free-firmware

Update installed packages

apt-get update && apt-get upgrade -y

Perform full upgrade (From 10 to 11)

apt full-upgrade -y

In the middle, there is license term confirmation, press q

Then, reboot

reboot

Remove old packages that will not use anymore after upgrade

apt autoremove -y

Prepare the folder for xray

mkdir /usr/local/etc
mkdir /usr/local/etc/xray

set VPS to use Google BBR as TCP congestion control

echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p
reboot

2. Get a Free DDNS (for example, freemyip)

Please remember the token you received (if you use freemyip), the following is an example:

example link

The above example, token is 7f5b79f4f0aaab7c33f04d8a, domain is testbunch.freemyip.com

Then, Install ddclient

apt-get -y install ddclient libdata-validate-ip-perl

Since some items are not shown, anything prompt, press enter We will reconfig ddelient below

dpkg-reconfigure ddclient

Option A. For freemyip (assume using example domain name)

  • Dynamic DNS service provider: select “other”.

  • Dynamic DNS update protocol: select "freemyip".

  • Dynamic DNS server: leave blank.

  • Optional HTTP proxy: leave blank.

  • Username: 7f5b79f4f0aaab7c33f04d8a

  • Password: 7f5b79f4f0aaab7c33f04d8a

  • Re-enter password: 7f5b79f4f0aaab7c33f04d8a

  • IP address discovery method: Web-based IP discovery service.

  • IP discovery services: ipify-ipv4 https://api.ipify.org/

  • Time between address checks: 5m

  • Hosts to update: testbunch.freemyip.com

Option B. For cloudflare (assume the full name is a.bname.com)

  • Dynamic DNS service provider: select “other”.

  • Dynamic DNS update protocol: select "cloudflare".

  • Dynamic DNS server: leave blank.

  • Optional HTTP proxy: leave blank.

  • Username: email address of cloudflare account.

  • Password: Cloudflare Global API key.

  • Re-enter password: Cloudflare Global API key.

  • IP address discovery method: Web-based IP discovery service.

  • IP discovery services: ipify-ipv4 https://api.ipify.org/

  • Time between address checks: 5m

  • Hosts to update: a.bname.com

Option B(cont.). Edit ddclient config (as zone should be missing)

nano /etc/ddclient.conf

Option B(cont.). Add the following line before the line of a.bname.com, then save

zone='bname.com' \

End of Option B.

Restart ddclient

systemctl restart ddclient

Try to ping the DDNS to check the IP, you might need to restart the ddclient a few times if the DDNS was just created seconds ago.

Check the DDNS IP match your server

3. Install NGINX as HTTPS page for fallback

The meaning of fallback is to fake GFW, if GFW tried to access the server without correct setup, a real https page will be shown You can host a your webpage if you want, folder location is /var/www/html/

Install NGINX

apt-get -y install nginx

Edit NGINX config to recognize acme folder and deny invalid access to folder view

nano /etc/nginx/sites-enabled/default
The result of nginx config file

Better copy it to notepad, then copy it from notepad again (as it might copy the background code in putty) You may ignore the 3 lines for acme-challenge if you use cloudflare, but you must add the deny all lines

*If you planning to install haproxy later, please change port 80 too.

listen 81 http2;

location ^~ /.well-known/acme-challenge/ {
  # the usual settings
}
 
location ~ /\. {
    deny all;
}

restart NGINX, create folder to store acme-challenge file Again, you can ignore the 2 lines which mkdir if you use cloudflare

systemctl restart nginx
mkdir /var/www/html/.well-known
mkdir /var/www/html/.well-known/acme-challenge

4. Install acme.sh and get SSL cert

apt-get -y install curl && curl https://get.acme.sh | sh && source ~/.bashrc

Register an account in ZeroSSL with email Change [email protected] to your email address

acme.sh --register-account -m [email protected]

Option A. for user that use freemyip only

Issue SSL cert to your system and install it to xray folder (Remember to change testbunch.freemyip.com to your ddns name) This method requires opening port 80 or port forward of port 80

acme.sh --issue -d testbunch.freemyip.com -w /var/www/html -k ec-384 --force
acme.sh --installcert -d testbunch.freemyip.com --fullchainpath /usr/local/etc/xray/fullchain.crt --keypath /usr/local/etc/xray/privkey.key --ecc --force

Option B. for user using cloudflare (non Proxied) only

You can generate cert with port 80 closed Issue SSL cert to your system and install it to xray folder, you can add more domain with -d when issue cert Never put your proxied CNAME record here. (Remember to change a.bname.co to your ddns name)

CF_Token=Generate a new Cloudflare API token with 2 Permission: Zone.Zone.Read, Zone.DNS.Edit You can set it can access all Zone, which you don't need to export zoneID here. Or you can set it access target zone and get the zone ID in Cloudflare webpage

export CF_Token="V8**************************"
export CF_Account_ID="61**************************"
export CF_Zone_ID="09**********************"
acme.sh --issue --dns dns_cf -d a.bname.co -k ec-384 --force
acme.sh --installcert -d a.bname.co --fullchainpath /usr/local/etc/xray/fullchain.crt --keypath /usr/local/etc/xray/privkey.key --ecc --force

Option C. for user using cloudflare (with CDN) only

Since there will be 2 domain name pointing to the same server, you need a cert that accept 2 names

b.bname.co is the proxied cname in Cloudflare

*Haproxy case, add \ after -force, next line type --reloadcmd "cat /root/.acme.sh/a.bname.co_ecc/fullchain.cer /root/.acme.sh/a.bname.co_ecc/a.bname.com.key | tee /etc/ssl/private/a.bname.co.pem"

export CF_Token="V8**************************"
export CF_Account_ID="61**************************"
export CF_Zone_ID="09**********************"
acme.sh --issue --dns dns_cf -d a.bname.co -d b.bname.co -k ec-384 --force
acme.sh --installcert -d a.bname.co --fullchainpath /usr/local/etc/xray/fullchain.crt --keypath /usr/local/etc/xray/privkey.key --ecc --force

End of Option parts

Schedule should be auto created, check with following Choose 1 (nano) when asked for choosing editor

crontab -e

There should be a line with command similar to following, if none, add it

48 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

5. Generate Cloudflare warp setting

Since Xray 1.7.0, xray support wireguard outbound connections. We can use Cloudflare warp to protect connection to CN instead of blocking all CN traffic

Create a folder to store Cloudflare warp config and download wgcf for generating config (Current version is 2.2.22 on 4 Jun 2024)

mkdir /usr/local/wgcf
cd /usr/local/wgcf
wget https://github.com/ViRb3/wgcf/releases/download/v2.2.22/wgcf_2.2.22_linux_amd64 -O wgcf

Modify permission of wgcf to be able to execute Then, register Cloudflare warp free account

chmod +x wgcf
./wgcf register

Generate Cloudflare warp config file

./wgcf generate

Show the config of Cloudflare warp, you need to save the following 2 items: PrivateKey, PublicKey

cat wgcf-profile.conf
Cloudflare Warp config example

With the above example PrivateKey: UI.......u/Wk= PublicKey: bm......yo=

6. Install and config Xray

Install Xray

bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install -u root

Generate UUID use for XRAY (Please remember the uuid, you will reuse it)

xray uuid
Remember to store the UUID for next step

Remove stock X-Ray config and create new one

rm /usr/local/etc/xray/config.json
nano /usr/local/etc/xray/config.json

Current config applies to Xray 1.8.13

Option A. For someone who will only use direct connection (No CDN setting)

Config description: Default using vless, xtls-rprx-vision, fallback to webpage if invalid If destination is CN domain or CN IP, use Cloudflare warp for outbound

Better copy it to notepad, then copy it from notepad again (as it might copy the background code in putty) Replace the following items: 90000000-0000-0000-0000-00000000000a : UUID you generated UI0000000000000000000000000000000000000u/Wk= : PrivateKey in 5 bm000000000000000000000000000000000000000yo= : PublicKey in 5

{
    "log": {
        "loglevel": "none"
    },
    "dns":{
        "servers":[
            "localhost"
        ],
        "queryStrategy":"UseIPv4"
    },
    "routing": {
        "domainStrategy": "IPIfNonMatch",
        "rules": [
            {
                "type": "field",
                "domain": [
                    "geosite:category-ads-all"
                ],
                "outboundTag": "block"
            },
	    {
                "type": "field",
                "domain": [
                    "geosite:cn"
                ],
                "outboundTag": "wireguard-1"
            },
            {
                "type": "field",
                "ip": [
                    "geoip:cn"
                ],
                "outboundTag": "wireguard-1"
            },
	    {
                "type": "field",
                "ip": [
                    "geoip:private"
                ],
                "outboundTag": "block"
            }
        ]
    },
    "inbounds": [
        {
            "port": 443,
            "protocol": "vless",
            "settings": {
                "clients": [
                    {
                        "id": "90000000-0000-0000-0000-00000000000a",
                        "flow": "xtls-rprx-vision"
                    }
                ],
                "decryption": "none",
                "fallbacks": [
                    {
                        "alpn": "http/1.1",
                        "dest":80
                    },
                    {
                        "alpn": "h2",
                        "dest": 81
                    }
                ]
            },
            "streamSettings": {
                "network": "tcp",
                "security": "tls",
                "tlsSettings": {
                    "minVersion":"1.3",
                    "alpn": [
                        "h2",
                        "http/1.1"
                    ],
                    "certificates": [
                        {
                            "certificateFile": "/usr/local/etc/xray/fullchain.crt",
                            "keyFile": "/usr/local/etc/xray/privkey.key",
                            "ocspStapling": 3600
                        }
                    ],
		    "rejectUnknownSni": true
                }
            },
	    "sniffing": {
		"enabled": true,
		"destOverride": [
		    "http",
		    "tls"
		]
	    }
        }
    ],
    "outbounds": [
        {
            "protocol": "freedom",
            "settings": {
                "domainStrategy": "UseIPv4"
            },
            "tag": "direct"
        },
        {
            "protocol": "wireguard",
            "settings": {
                "secretKey" : "UI0000000000000000000000000000000000000u/Wk=",
                "peers": [
                    {
                        "publicKey": "bm000000000000000000000000000000000000000yo=",
                        "endpoint": "engage.cloudflareclient.com:2408"
                    }
                ]
            },
            "tag": "wireguard-1"
        },
        {
            "protocol": "blackhole",
            "tag": "block"
        }
    ]
}

Option B. For someone who will use CDN for backup connection (Cloudflare Proxied)

Config description: Default using vless, xtls-rprx-vision, fallback to webpage if invalid If path /fallws is provided, fallback to use ws, and it only accept proxy protocol connection (for example Cloudflare CDN) If destination is CN domain or CN IP, use Cloudflare warp for outbound

Better copy it to notepad, then copy it from notepad again (as it might copy the background code in putty) Replace the following items: 90000000-0000-0000-0000-00000000000a : UUID you generated (2 instance) /fallws : the folder you name to tell xray fallback using ws/CDN (2 instance) UI0000000000000000000000000000000000000u/Wk= : PrivateKey in 5 bm000000000000000000000000000000000000000yo= : PublicKey in 5

Note: there are 6 lines with "#" in front, if you use reverse proxy (like HAProxy) to handle all traffic in TCP443, and you enabled "Proxy Protocol" in that config. Remove those "#" in this config. If you don't know what I'm talking about, please ignore this

{
    "log": {
        "loglevel": "none"
    },
    "dns":{
        "servers":[
            "localhost"
        ],
        "queryStrategy":"UseIPv4"
    },
    "routing": {
        "domainStrategy": "IPIfNonMatch",
        "rules": [
            {
                "type": "field",
                "domain": [
                    "geosite:category-ads-all"
                ],
                "outboundTag": "block"
            },
	    {
                "type": "field",
                "domain": [
                    "geosite:cn"
                ],
                "outboundTag": "wireguard-1"
            },
            {
                "type": "field",
                "ip": [
                    "geoip:cn"
                ],
                "outboundTag": "wireguard-1"
            },
	    {
                "type": "field",
                "ip": [
                    "geoip:private"
                ],
                "outboundTag": "block"
            }
        ]
    },
    "inbounds": [
        {
            "port": 443,
            "protocol": "vless",
            "settings": {
                "clients": [
                    {
                        "id": "90000000-0000-0000-0000-00000000000a",
                        "flow": "xtls-rprx-vision"
                    }
                ],
                "decryption": "none",
                "fallbacks": [
                    {
                        "alpn": "http/1.1",
                        "dest":80
                    },
                    {
                        "path": "/fallws",
                        "dest": 1234,
                        "xver": 1
                    },
                    {
                        "alpn": "h2",
                        "dest": 81
                    }
                ]
            },
            "streamSettings": {
                "network": "tcp",
                "security": "tls",
#                "tcpSettings":{
#                    "acceptProxyProtocol":true
#                },
                "tlsSettings": {
                    "minVersion":"1.3",
                    "alpn": [
                        "h2",
                        "http/1.1"
                    ],
                    "certificates": [
                        {
                            "certificateFile": "/usr/local/etc/xray/fullchain.crt",
                            "keyFile": "/usr/local/etc/xray/privkey.key",
                            "ocspStapling": 3600
                        }
                    ],
		    "rejectUnknownSni": true
                }
            },
	    "sniffing": {
		"enabled": true,
		"destOverride": [
		    "http",
		    "tls"
		]
	    }
        },
        {
            "port": 1234,
            "listen": "127.0.0.1",
            "protocol": "vless",
            "settings": {
                "clients": [
                    {
                        "id": "90000000-0000-0000-0000-00000000000a",
                        "level": 0
                    }
                ],
                "decryption": "none"
            },
            "streamSettings": {
                "network": "ws",
#                "tcpSettings": {
#                    "acceptProxyProtocol": true
#                },
                "security": "none",
                "wsSettings": {
                    "acceptProxyProtocol": true,
                    "path": "/fallws"
                }
            },
	    "sniffing": {
		"enabled": true,
		"destOverride": [
		    "http",
		    "tls"
		]
	    }
        }
    ],
    "outbounds": [
        {
            "protocol": "freedom",
            "settings": {
                "domainStrategy": "UseIPv4"
            },
            "tag": "direct"
        },
        {
            "protocol": "wireguard",
            "settings": {
                "secretKey" : "UI0000000000000000000000000000000000000u/Wk=",
                "peers": [
                    {
                        "publicKey": "bm000000000000000000000000000000000000000yo=",
                        "endpoint": "engage.cloudflareclient.com:2408"
                    }
                ]
            },
            "tag": "wireguard-1"
        },
        {
            "protocol": "blackhole",
            "tag": "block"
        }
    ]
}

End of Options part

Since XRay will not load new cert automatically if cert updated, you need to restart xray after cert updated.

crontab -e

Add the following to the bottom line to restart xray everyday 4 AM

0 4 * * * /bin/systemctl restart xray

Then, restart XRay

systemctl restart xray

You can check xray status with following command

systemctl status xray

The result will be something like below, which shows it is running and the version of xray

Last updated

Was this helpful?