全文参考:

原理

VLESS 作为内部传输协议,使用 HTTP 的扩展 WebSocket 作为传输载体,外层使用 TLS 加密传输。服务器端用 Nginx 搭建正常 HTTPS 网站,收到向特定路径发送的 WebSocket 流量时充当反向代理转发至 Docker 容器内的 V2Ray 处理。

从外部看,服务器是货真价实的 HTTPS 服务器,客户端发出的请求也是货真价实的 HTTPS 流量;直接用浏览器访问入口路径将返回 400 Bad Request(由 V2Ray 返回)或 404 Not Found(可在 Nginx 中预检测 WebSocket 请求,增强隐蔽性);安全性、抗干扰能力则完全由 TLS 保障,几乎无懈可击。

更强的技术还有 Xray 中使用的 XTLS,无缝拼接了内外两层 TLS 使得性能进一步提高。不过因为和 V2Ray 主社区分离了,还是决定先用 VLESS+WS+TLS+Web 配置。

结构上,使用 Docker 和 Docker Compose 容器化安装方便管理;而装在外层的 Nginx 可以作为所有网页服务的入口点,方便以后增加其它服务。

流程

以下所有操作在 Ubuntu 20.04 的 root 用户下进行。代码中部分需要自己填写的敏感信息会用如 {[VARIABLE]} 的记号标明,请自行替换。

准备工作

安装 cURL

apt install curl

安装 Docker

curl -fsSL https://get.docker.com/ | sh # 获取脚本并交给 sh (shell) 执行
systemctl start docker

安装 Docker Compose

curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
# docker-compose --version

安装并运行 Nginx

apt install nginx
systemctl start nginx

此时通过浏览器访问服务器就能看到 Nginx 默认的欢迎界面了。

调试技巧

Nginx 的日志存储在 /var/log/nginx 目录下;V2Ray 的日志在 Docker 容器里,之后我们将把它映射到宿主机上方便操作和存储。

一些常用的调试命令:

systemctl start nginx
systemctl status nginx
systemctl stop nginx

docker-compose up -d # Create and start containers(-d 表示后台运行)
docker-compose down  # Stop and remove containers, networks, images, and volumes
docker-compose start # Start services
docker-compose stop  # Stop services

docker ps # 查看各容器运行状态
docker log {[CONTAINER_ID]} # 查看某容器运行日志

安装 Certbot 并以 Webroot 方式获取 Let's Encrypt 证书

Let's Encrypt 是一个免费、自动化和开放的证书颁发机构,为网站提供免费的 SSL/TLS 证书。要从 Let's Encrypt 获取某个域名的证书,需要证明拥有对该域名的控制权,而 Certbot 就是官方提供的自动化认证工具。

Webroot 是 Certbot 提供的一种认证方式,如果服务器上有网站运行且有能力修改其配置,就可以用该方式进行认证。使用这种方式获取证书时无需暂停网页服务端的运行。

The webroot plugin works by creating a temporary file for each of your requested domains in ${webroot-path}/.well-known/acme-challenge. Then the Let’s Encrypt validation server makes HTTP requests to validate that the DNS for each requested domain resolves to the server running certbot.

首先安装 Certbot:

apt install certbot python3-certbot

因为之前安装的 Nginx 已经在 /var/www/html 下生成默认的欢迎页网站,故可直接利用该目录进行 Webroot 认证:

certbot certonly --webroot -w /var/www/html -d {[YOUR_DOMAIN]}

认证过程中 Certbot 的回显信息:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for {[YOUR_DOMAIN]}
Using the webroot path /var/www/html for all unmatched domains.
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/{[YOUR_DOMAIN]}/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/{[YOUR_DOMAIN]}/privkey.pem
   Your cert will expire on {[EXPIRE-DATE]}. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

使用 Docker Compose 安装并配置 V2Ray

上传 docker-compose.json 至适当位置(建议妥善储存该文件,以便后续管理使用):

version: "3.4"
services:
  v2ray:
    image: v2fly/v2fly-core
    container_name: v2ray
    restart: always # 自动重启容器
    ports: 
      - {[YOUR_PORT_OUTER]}:{[YOUR_PORT_INNER]} # 端口映射,注意外左内右
    command: v2ray -config=/etc/v2ray/config.json # 需要指定配置文件位置
    volumes:
      - /etc/v2ray:/etc/v2ray # 左边宿主目录,右边容器目录
      - /var/log/v2ray:/var/log/v2ray

利用 volume 技术,容器中 V2Ray 的配置文件被映射到宿主机的 /etc/v2ray 目录下,而日志信息被映射到 /var/log/v2ray 目录下。

故运行容器之前,我们在 /etc/v2ray 下放置 config.json 作为容器内 V2Ray 的配置文件:

{
    "log": {
        "loglevel": "warning",
        "access": "/var/log/v2ray/access.log",
        "error": "/var/log/v2ray/error.log"
    },
    "inbounds": [
        {
            "port": "{[YOUR_PORT_INNER]}", // 容器内的监听端口
            "listen":"0.0.0.0", // Caution!
            "protocol": "vless",
            "settings": {
                "decryption": "none",
                "clients": [
                    {
                        "id": "{[YOUR_UUID]}",
                        "email": "{[YOUR_EMAIL]}" // 作标识用,可以随便填
                    }
                ]
            },
            "streamSettings": {
                "network": "ws",
                "wsSettings": {
                    "path": "{[YOUR_PATH]}" // 希望设定的 V2Ray 入口路径,如 "/ray"
                }
            }
        }
    ],
    "outbounds": [
        {
            "protocol": "freedom",
            "settings": {}
        }
    ]
}

有一个小坑点(见 Issue #2221):与白话文指南不同,如果用 Docker 搭建 V2Ray,容器外的 Nginx 需要向容器内的 V2Ray 发送数据,因此容器内的 V2Ray 必须监听本机 IP 0.0.0.0 而不是本地回环 IP 127.0.0.1

常见的症状是客户端报 502 Bad Gateway > websocket: bad handshake ,Nginx 报 upstream prematurely closed connection,而容器内 V2Ray 没有报警日志。

最后,在之前放置 docker-compose.yml 的目录下执行:

docker-compose up -d

设置 Nginx 反向代理

通常修改 Nginx 配置可以通过直接修改 /etc/nginx/nginx.conf 或在 /etc/nginx/conf.d/ 目录下新建配置文件的方式进行,但根据推荐的目录结构(参见 Nginx/DirectoryStructure - Debian Wiki),这里选择在 /etc/nginx/sites-available/ 放置配置文件后在 /etc/nginx/sites-enabled/my-enabled.conf 中动态引用。

故首先在 /etc/nginx/sites-available/ 下新建 {[YOUR_DOMAIN]}.conf(文件名可自行调整):

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate       /etc/letsencrypt/live/{[YOUR_DOMAIN]}/fullchain.pem;
  ssl_certificate_key   /etc/letsencrypt/live/{[YOUR_DOMAIN]}/privkey.pem;
  # 利用缓存重用 session 提高性能
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;
  ssl_session_tickets off;

  ssl_protocols         TLSv1.2 TLSv1.3;
  # 设置加密方式,默认的已经不安全了
  ssl_ciphers           ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off; # (?) Specifies that server ciphers should be preferred over client ciphers when using the SSLv3 and TLS protocols.

  server_name           {[YOUR_DOMAIN]};
  location {[YOUR_PATH]} { # 与 V2Ray 配置中的 path 保持一致
    if ($http_upgrade != "websocket") { # WebSocket 协商失败时返回 404
        return 404;
    }
    proxy_redirect off;
    proxy_pass http://127.0.0.1:{[YOUR_PORT_OUTER]}; # 设置反向代理转发至 V2Ray
    proxy_http_version 1.1;
    # 一些 WebSocket 需要的配置
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    # Show real IP in v2ray access.log
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
# modified from (2022/01/30) https://guide.v2fly.org/advanced/wss_and_web.html#nginx-%E9%85%8D%E7%BD%AE

然后在 /etc/nginx/sites-enabled/ 目录下,先删除原有的 default 文件,然后新建 my-enabled.conf(文件名可自行调整):

include /etc/nginx/sites-available/default; # 继续使用 Nginx 的默认站点配置文件
include /etc/nginx/sites-available/{[YOUR_DOMAIN]}.conf; # 引用刚刚新建的配置文件
  • Update: 实际上推荐的方式是用 ln -s/etc/nginx/sites-enabled/ 里创建 symbolic link,当时搞的时候还不太懂,是我 naive 了(

最后重新加载配置文件:

nginx -s reload

客户端

用的图形化界面还没研究配置文件,不过 outbounds 部分大致应如下所示:

"outbounds": [
    {
        "protocol": "vless",
        "settings": {
            "vnext": [
                {
                    "address": "{[YOUR_DOMAIN]}",
                    "port": 443,
                    "users": [
                        {"id": "{[YOUR_UUID]}", "email": "{[YOUR_EMAIL]}"}
                    ]
                }
            ]
        },
        "streamSettings": {
            "network": "ws",
            "security": "tls",
            "wsSettings": {
                "path": "{[YOUR_PATH]}"
            }
        }
    }
]