Self-hosted: Xray Encrypted Proxy

More often than not, one may find their network provider blocking, censoring, or monitoring their network traffic. For various reasons, they may want to not be blocked, censored, or monitored while accessing the internet.

This post is a tutorial that allows the reader to set up their own obfuscated proxy server to circumvent internet filters. In particular, we’ll use the xray proxy protocol and server, which is a nearly undetectable proxy server that uses encryption similar to Transport Layer Security (TLS) to have the proxy traffic blend in with regular web traffic. This means that it will work even if the network is configured to detect and block proxies.

We will need a Linux server with ports exposed to the internet to get started. This guide is a condensed version of this tutorial found in the xray documentation.

TLS Certificate

Our proxy server requires a TLS encryption tunnel to securely function. In order to establish a TLS tunnel, we need a TLS certificate. To obtain a TLS certificate, we will need to complete a series of steps. First, port forward the host on ports 80 and 443.

Next, we need a domain name and a web server on our machine to request the certificate (either top-level domains or subdomains, they both work). For the former, a free subdomain can be obtained at FreeDNS by signing up, if the reader does not have a domain name to use. Make sure the domain resolves to the host’s public IP address.

For the web server, we will install nginx. This way our certificate gets approved correctly when the certificate authority (CA) gives us a challenge. It is also recommended that one hosts a website on the server for security purposes (to disguise the fact that we are running a proxy, should someone decide to check by connecting to our domain name), but it is not necessary. Our goal right now is that we just need this website to expose certain directories on our machine to satisfy the TLS certificate challenge requirements from the CA.

$ sudo apt update
$ sudo apt install nginx

In the nginx config, we’ll need to change the server block to handle requests to our domain to prove to the CA that we actually own our domain. Change the server_name1 below to our domain and root directory to somewhere we have write permissions to:

$ sudoedit /etc/nginx/sites-available/default
server {
    listen 80 default_server;
    listen[::]:80 default_server;
    # ...
    root /home/admin/webroot;
    server_name subdomain.domain.com;
}
$ sudo systemctl restart nginx
$ sudo systemctl enable nginx

We’ll also need to ensure that the nginx user, www-data by default, has read and write permissions to the web root. This way the web server can read and serve the files in the directories. One may also need to change the permissions of parent directories as well (for example, the home directory if the web root is inside it):

$ sudo chmod a+r /home/admin/webroot

Finally, we can request our certificates. We’ll use the acme protocol to request and renew our certificate for free with acme.sh and Let’s Encrypt. Install acme.sh and its aliases:

$ curl https://get.acme.sh | bash
$ source .bashrc

Before actually requesting the certificate, we should first issue a test certificate. This way, we can avoid repeatedly requesting certificates due to local configuration errors, resulting in us hitting the rate limit for Let’s Encrypt. Afterward, if no issues occur during the test request, we can proceed to issue the actual certificate. Note that the domain and the path in the commands below should be the same ones entered in the nginx configuration file:

# Test issuing the certificate. 
$ acme.sh --issue --server letsencrypt_test -d subdomain.domain.com -w /home/admin/webroot
# If everything was successful, issue the actual certificate. Otherwise, try
# browsing through acme.sh's documentation to see if you can fix the issue:
# https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
$ acme.sh --set-default-ca --server letsencrypt
$ acme.sh --issue -d subdomain.domain.com -w /home/admin/webroot --keylength ec-256 --force

Install the certificate and save the paths for use later. Because the key file isn’t readable to other users, and our proxy server runs on the user nobody by default (feel free to change it), we will also need to edit its read permissions:

$ acme.sh --installcert -d subdomain.domain.com --key-file /home/admin/cert.key --fullchain-file /home/admin/fullchain.crt --ecc
$ chmod +r /home/admin/cert.key # Add read permissions

Proxy Server

Now we can finally get to setting up the xray proxy server. As alluded to earlier, xray uses encryption similar to TLS to obfuscate the traffic, thereby making it mimic regular web traffic characteristics to any observing middle person. This is the reason why we first needed an actual TLS certificate issued by a CA, because we need to make the handshake process will look legitimate. To install the xray server:

$ curl -fsSL https://github.com/XTLS/Xray-install/raw/main/install-release.sh | sudo bash

Generate a UUID to save for later2:

$ xray uuid

Then, edit the xray configuration file. Below is a general template for a bare bones working config. In order to use it, one should put their generated UUID in the “inbound > clients” section:

$ sudoedit /usr/local/etc/xray/config.json
{
    // The DNS servers that your proxy server will use to query requests
    // from clients. For example, if a client connects to google.com
    // through your proxy, google.com will be resolved using the
    // following DNS servers.
    "dns": {
        "servers": [
            "localhost",
            "1.1.1.1",
            "8.8.8.8"
        ]
    },

    // Categorizing and tagging different types of client connections.
    // For example, the following tags private IP, known advertizers,
    // and torrenting protocol as "block". You can specify how to deal
    // with each of the categories in the outbounds object below. 
    "routing": {
        "domainStrategy": "IPIfNonMatch",
        "rules": [
            {
                "type": "field",
                "ip": ["geoip:private"],
                "outboundTag": "block"
            },
            {
                "type": "field",
                "domain": ["geosite:category-ads-all"],
                "outboundTag": "block"
            },
            {
                "type": "field",
                "protocol": ["bittorrent"],
                "outboundTag": "block"
            }
        ]
    },

    // Handling client connections to the proxy. You should not change
    // the port or the protocol, because they are integral to blending
    // your proxy connections in HTTPS traffic. You can also add config
    // for multiple clients in the client section below. This will be
    // useful if you want to share your proxy server with others. 
    "inbounds": [
        {
            "port": 443,
            "protocol": "vless",
            "settings": {
                "clients": [
                    {
                        "id": "",           // Put your UUID here! 
                        "flow": "xtls-rprx-vision",
                        "email": "admin",   // The email is mostly for
                        "level": 0          // identification purposes
                    }                       // in the logs. You don't
                ],                          // actually need to put
                                            // a valid email here. 
                "decryption": "none",
              
                // If the inbound traffic is not the vless protocol, we
                // want to fallback to the nginx web server. This can
                // help us disguise our proxy server as an actual
                // website.
                "fallbacks": [{
                    "dest": 80,
                    "xver": 1
                }]
            },

            // These should be pretty self-explanatory TLS encryption
            // settings. Make sure to change the certificate paths. 
            "streamSettings": {
                "network": "tcp",
                "security": "tls",
                "tlsSettings": {
                    "alpn": "http/1.1",
                    "minVersion": "1.2",
                    "maxVersion": "1.3",
                    "certificates": [{
                        "certificateFile": "/home/admin/fullchain.crt",
                        "keyFile": "/home/admin/cert.key"
                    }]
                }
            }
        }
    ],

    // The outbounds section dictates how client traffic is processed
    // based on the tags specified in the inbound section. Any traffic
    // that isn't tagged will be sent to the first outbound. For more
    // information, see the "outbounds" section on the sidebar here:
    // https://xtls.github.io/en/config/outbounds/blackhole.html
    "outbounds": [
        {
            "domainStrategy": "AsIs",
            "tag": "direct",
            "protocol": "freedom"
        },
        {
            "tag": "block",
            "protocol": "blackhole"
        }
    ]
}
$ sudo systemctl enable xray
$ sudo systemctl restart xray

The reader can also check out the configuration documentation for xray for further info.

Proxy Client

Now that the hard part is finished, we can connect to our new proxy server with clients. The clients that I personally suggest are:

Different clients will have different user interfaces, so it is unrealistic for me to cover how to use each and every client in this blog post. However, I will list out some common settings one may encounter and what those settings will be using our example configuration file:

FieldValue
Address/SNIsubdomain.domain.com1
Flowxtls-rprx-vision
ALPNhttp/1.1
Port443
Protocolvless
NetworkTCP
SecurityTLS
UUID<Your UUID here>

Finally, note that no obfuscation method is entirely foolproof. There are detection techniques such as DPI amongst many others that may expose one’s proxy server. Thus, even with encryption, one must use proxies with careful judgement to avoid detection.

Footnotes


  1. The domain subdomain.domain.com is used as an example domain throughout this post. Put your actual domain in these fields. ↩︎ ↩︎

  2. The UUID generated is not tied to xray. In other words, you can use any other UUID generators and it would work fine. ↩︎