【进阶向】Cloudflare 自定义主机名单域名优选与 Nginx SNI 分流详解

原文来源:NodeSeek - 通过Cloudflare的自定义主机名功能进行单域名优选


一、背景

(一)前言

很多人买了 VPS,也用 Cloudflare 大善人提供的服务,同时 Nginx 是一个比较容易上手的高性能 Web 服务器,用它来进行反向代理配置,而且还有更好用的 Nginx Proxy Manager 配置更是简单(虽然我不用),今天只是通过 Nginx 来讲解原理,其他的 Web 服务器参考原理来进行配置。

用到 Cloudflare 和 Nginx 常常会有很多需求,譬如:

  1. 隐藏机器 IP 防止被打;
  2. 加速境内访问自建站点的速度;
  3. 其他用途(各位应该都懂,具体就不展开说了)。

(二)开始今天的教程之前,需要做好以下准备

准备条件:

  1. 拥有一个在 Cloudflare 进行解析的开启了小黄云 CDN 的域名作为源站域名(下文用自有域名 test.isakura.in 来演示)。
  2. 已经使用源站域名在自己的服务器通过 Nginx 部署了一个网站(最好配置了有效证书)。
  3. 拥有一个提供给别人访问的外部域名(因为我没有多余的域名,我将在 Cloudflare 解析 abcd.isakura.in 作为我的外部域名,如果有条件的也可以用其他服务商托管解析的域名)。

也因此本文是单域名优选教程


(三)最终达成效果

最终效果:

  1. 浏览器访问 https://abcd.isakura.in ,最终显示网站的内容;
  2. 浏览器访问 https://test.isakura.in ,最终返回 404;
  3. 优选 IP 或者域名,加速网站的访问。

(四)通过这次实战你会了解

通过这次实战你会了解:

  1. CDN 数据流向过程;
  2. Nginx 中实现的 SNI 分流;
  3. 数据包传递中请求头 host 和 SNI 的作用。

二、引入的前置知识

(一)Nginx 的配置文件(每一行代码都有注释)

1. Nginx 的配置文件主要是以代码块的形式存在,主要有全局代码、events 块、stream 块和 http 块。这些块里面又可以含有其他代码块,其中我们需要用的是 stream 块http 块的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 配置文件总览

# 1. 部分全局代码

# 2. events块
events {
# 主要配置网络连接参数等等内容
}

# 3. stream块
stream {
# 主要配置OSI七层模型中传输层的协议(L4)如tcp、udp等等
}

# 4. http块
http {
# 主要配置OSI七层模型中应用层的协议(L7)如http
}

2. 部署一个普通网站的代码块,仅用到 http 块就好了(已省略不必要参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
server {
listen 80; # 监听80端口
return 301 https://$http_host$request_uri; # 收到80端口的http请求重定向为访问443端口的https请求
}
server {
listen 443 ssl; # 监听443端口,并启用ssl/tls加密
server_name test.isakura.in; # 监听域名test.isakura.in
root /path/to/web; # 用于指定网站的资源文件在/path/to/web目录上
index index.html index.php; # 用于指定网站的默认首页应该读取/path/to/web目录下的index.html或者index.php文件
location / {
try_files $uri $uri/ =404;
}
# location块是根据请求的URI,匹配相关路径的资源
# (譬如访问https://test.isakura.in/abcd,就会看location /abcd {}下指定的文件资源,没有就在location / {}下来找)
}
}

3. 通过 stream 模块来进行域名 SNI 分流,分流后将请求网站的流量送回 http 块(已省略不必要参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
stream {
# $ssl_preread_server_name是一个内置变量,用于记录进入nginx的流量的SNI值
map $ssl_preread_server_name $variable {
abcd.isakura.in abcd;
test.isakura.in test;
}
# 该map块用于映射,用abcd对应代表abcd.isakura.in,用test对应代表test.isakura.in
# 同时将这两个对应关系记录进变量$variable

server {
listen 443 reuseport; # 监听443端口,并且进行端口复用
ssl_preread on; # 不解密流量情况下,预先读取SNI值
proxy_pass $variable; # 反向代理,指接收到443端口流量会根据预读取的SNI访问$variable变量指代的值(abcd/test)
}

upstream abcd {
server 127.0.0.1:8080;
}
# 用abcd指代服务器 127.0.0.1:8080,也就是访问abcd等于访问127.0.0.1:8080,后续不配置8080端口监听服务了

upstream test {
server 127.0.0.1:8081;
}
# 用test指代服务器 127.0.0.1:8081,也就是访问test等于访问127.0.0.1:8081
}

# http模块用于在stream模块分流后,监听真实的服务
# 譬如此处监听本机的8081端口,对应test.isakura.in的网站
http {
server {
listen 80;
return 301 https://$http_host$request_uri;
}

server {
listen 127.0.0.1:8081 ssl; # 监听127.0.0.1地址的8081端口,并启用ssl/tls加密
server_name test.isakura.in;
root /path/to/web;
index index.html index.php;
location / {
try_files $uri $uri/ =404;
}
}
}

(二)自定义主机名功能(来自 SaaS,此功能的开通需要验证支付方式)

自定义主机名功能中,可以自行定义一个主机名 A,当 Cloudflare 接收到访问 A 的请求时,将这个访问请求回退到一个自定义源服务器 B。注意,B 的域名必须有托管在 Cloudflare,进行了解析并开启了小黄云的 A 记录、AAAA 记录、CNAME 记录。

上面这句话要注意几个点:

  1. B 已经是在 Cloudflare 进行了小黄云解析的了,因此 B 已经隐藏了其真实 IP
  2. 需要的是 Cloudflare 接收到访问 A 主机名的请求,也就是 Cloudflare 的 CDN 收到了访问 A 主机名的请求;
  3. 在收到 Cloudflare 的 CDN 收到请求后,进行回退到源服务器的操作,并且会根据选择的自定义源服务器,回退数据包的 SNI 值

(三)关于数据包的 SNI 值和 host 值

  • 访问网站的数据包在到达服务器时,在传输层使用 TCP 协议,在应用层使用的是 HTTP 协议。
  • SNI(Server Name Indication)是 TLS 协议的扩展字段,在 HTTPS 连接的 TLS 握手阶段发送,目的是让服务器在建立加密连接前,就知道客户端想要访问的具体主机名。但如果没有加密连接场景,SNI 值则不会存在(此处不考虑该情况)。
  • host 值是 HTTP 协议的请求头之一,用于在应用层指定客户端想要访问的网站域名。
  • 服务器会先在 TLS 握手时解析 SNI 值来确立加密连接,随后才会解密 HTTP 数据,读取 host 值来处理具体的网页请求。所以从时间顺序上来说,SNI 应该是优先被处理,而后续根据 HTTP 请求头中的 host 值来访问具体网站内容,因此 SNI 也充当了路由的功能。这两个值一般来说是一致的,但在优选场景下 SNI 值出现了变更

优选场景下的数据流向:

1
2
3
4
5
6
7
graph LR
A[用户访问 abcd.isakura.in] --> B[DNS 解析到优选 IP]
B --> C[Cloudflare CDN 收到请求]
C -->|host: abcd.isakura.in| D[回退到源服务器]
C -->|SNI: test.isakura.in| D
D --> E[Nginx stream 按 SNI 分流]
E --> F[Nginx http 按 host 返回内容]

正常访问时 SNI 和 host 一致,但经过自定义主机名 + 优选配置后,SNI 变为源服务器域名,host 保持为自定义主机名。这就是 SNI 分流的基础。


(四)优选 IP / 域名

其实就是在中国大陆访问延迟低、速度快、相对稳定的 Cloudflare CDN 的 IP / 解析到 Cloudflare CDN 的 IP 的域名 / 甚至是反代了 Cloudflare CDN 的 IP(譬如 Reality 的 IP)。


(五)实战前我的基础配置

Cloudflare 配置:

Cloudflare 配置 1

Cloudflare 配置 2

Nginx 配置(已部署证书,并略去多余配置):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
stream {
map $ssl_preread_server_name $map {
test.isakura.in test;
# 其余映射略
}
server {
listen 443 reuseport;
ssl_preread on;
proxy_pass $map;
}
upstream test {
server 127.0.0.1:8081;
}
upstream others {
# 此处略去其他反代配置
}
}

http {
server {
listen 80;
return 301 https://$http_host$request_uri;
}
server {
listen 127.0.0.1:8081 ssl;
server_name test.isakura.in;
root /usr/local/testdir; # 我的网页存放在/usr/local/testdir
index index.html;
location / {
try_files $uri $uri/ =404;
}
# ssl加密验证配置略,已经配置了证书
}
}

网站文件配置:

我的主机在 /usr/local/testdir 下存放了一个 index.html

网站文件目录

它的代码如下(AI 写的),访问的时候会在页面中间显示当前 host 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>显示 Host 值</title>
<style>
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: sans-serif;
}
h1 { margin-bottom: 20px; }
#hostDisplay {
font-size: 2em;
color: blue;
}
</style>
</head>
<body>
<h1>当前请求的 Host 是:</h1>
<div id="hostDisplay"></div>

<script>
const host = window.location.host;
document.getElementById('hostDisplay').textContent = host;
</script>
</body>
</html>

访问效果如下:

访问效果


三、实战操作(含 Cloudflare 优选、Nginx 分流配置、保护源域名)

(一)添加自定义主机名,设置回退到源服务器

原理: 这一步的意义是让 Cloudflare 知道,如果我们的访问自定义主机名请求给到 Cloudflare,那么 Cloudflare 知道要访问哪个源去获取资源

1. 添加一个默认回退源服务器

此处我将 test.isakura.in 作为回退源服务器添加进去,如果 B 满足上述条件,应该添加进去几秒钟后刷新就有效。

添加回退源服务器

2. 添加自定义主机名

此处是 abcd.isakura.in,自定义源服务器必须选「自定义源服务器」且需要填回退的源服务器 test.isakura.in

添加自定义主机名 1

添加自定义主机名 2

添加自定义主机名 3

添加自定义主机名 4

关于自定义源服务器:

  • 默认源服务器: 回退到默认源服务器,且 SNI 值会被设定为「自定义主机名 host 值」
  • 自定义源服务器: 回退到自定义源服务器,且 SNI 值会被设定为「自定义源服务器」

注意: 想要为同为 Cloudflare 的域名优选,如果选择默认源服务器,则当回退时,Cloudflare 会检测到你的自定义主机名所在域是 Cloudflare 管理的域,同时会根据 SNI 将请求再次返回自定义主机名,会造成 Error 1000,因此如果是在 Cloudflare 中进行优选,则不能选择默认源服务器。

关于主机名状态:

如果这个主机名是在 Cloudflare 的顶级域归属 Cloudflare 解析,那么主机名状态会自动变为「有效」,否则需要在解析商添加 TXT 验证。

关于证书验证方法:

  • HTTP 验证: 是一种自动验证方式,要满足几个条件。首先要满足不是泛域名,然后这个主机名要么是归属 Cloudflare 的,要么它已经由 Cloudflare 来代理流量了。
  • TXT 验证: 上述 HTTP 验证方法不行的情况下,都需要使用 TXT 验证,就是在主机名的解析商添加解析记录进行验证。

3. TXT 方式验证证书(有条件的建议使用 HTTP 验证,此处仅演示)

TXT 验证

TXT 验证记录

过一会刷新状态,证书变为有效:

证书有效


(二)为自定义主机名添加 DNS 解析,解析到 Cloudflare 的 CDN 上(优选的重要步骤)

原理: 虽然 Cloudflare 知道如果它接收到这个访问自定义主机名的请求时,需要访问源服务器获取资源,但这个请求从哪里来呢?因此这一步就是为这个自定义主机名进行解析,让自定义主机名的请求能解析到 Cloudflare 的 CDN 上这一步也是优选的关键步骤,落到哪个 CDN 上是由你来定

附: 此步骤可以在其他域名解析服务商里面进行,如果在国内的服务商还可以实现国内外线路分流。

1. 找一个优选的 IP / 域名

这个你们自己去找,网上应该很多,我直接就用一个域名了(*.cloudflare.182682.xyz),它含三线解析。

2. 为自定义主机名添加解析,解析到优选 IP / 域名上

添加解析

小技巧: 如果是优选的域名,也可以添加一个 CNAME 记录 cdn.isakura.in 指向 abcd.cloudflare.182682.xyz,这样以后如果再需要为自定义添加优选域名的话,就可以直接添加 CNAME 记录到 cdn.isakura.in,也方便后续更改优选域名,只要更改 cdn.isakura.in 的 CNAME 记录即可。

CNAME 配置

至此,优选已经完成! 但是我们访问 abcd.isakura.intest.isakura.in 均能访问。为了我们的服务器安全,还需要做几步。


(三)在 Nginx 上配置 SNI 分流,过滤大部分无效请求

原理: 正常访问的 HTTP 数据包,默认的 SNI 字段和 HTTP 请求头中的 host 是一样的。但经过上面的设置后,访问自定义主机名的请求,最终到达服务器时,host 值不改变,SNI 值变为源服务器域名

上面已经设置了自定义主机名是 abcd.isakura.in,源服务器是 test.isakura.in,因此访问 https://abcd.isakura.in 时,当请求来到我的服务器,这个数据的 host 值为 abcd.isakura.in,SNI 值为 test.isakura.in

因此进行分流需要设置 stream 块的值,我之前的默认设置已经是配好了,不需要改动

分流的关键设置是在 stream 中的 map 块对需要预读的 SNI 进行限定,这样 server 块对其进行反向代理时,不属于 map 块中出现的 SNI 将会被拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stream {
map $ssl_preread_server_name $map {
test.isakura.in test;
# 其余映射略,在此对需要预读的SNI进行限定
}
server {
listen 443 reuseport;
ssl_preread on;
proxy_pass $map;
}
# 该stream的server块就会默认根据$map变量分流,只需要在上面的map块进行sni配置即可
upstream test {
server 127.0.0.1:8081;
}
upstream ...... {
# 此处略去其他反代配置......
}
}

(四)让 Nginx 只接受访问自定义主机名的请求,拒绝其他请求

原理: 第(三)步已经通过 SNI 筛选了大部分请求,但是并没有根据 host 筛选请求。现在需要在 http 块中设置根据 host 值判断是否返回内容,如果 host 值为自定义主机名,则返回内容,否则不返回。

根据 host 值来进行判断,就是应用层的事情了,在 Nginx 配置的就是 http 块,需要在 location 中添加一段 if 判断条件的配置,当 host 不等于 abcd.isakura.in 时,返回 404。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
server {
listen 80;
return 301 https://$http_host$request_uri;
}
server {
listen 127.0.0.1:8081 ssl;
server_name test.isakura.in;
root /usr/local/testdir; # 我的网页存放在/usr/local/testdir
index index.html;
location / {
try_files $uri $uri/ =404;
if ($host != "abcd.isakura.in") {
return 404;
}
# 这里是配置的关键,host值不等于abcd.isakura.in时,返回404
}
# ssl加密验证配置略,已经配置了证书
}
}

经过以上设置,只有 host 为 abcd.isakura.in 的请求能被响应,**test.isakura.in 的请求失效**:

源域名保护完成!现在直接访问 test.isakura.in 会返回 404,只有通过自定义主机名 abcd.isakura.in 才能访问网站。

test.isakura.in 返回 404


(五)检查 Cloudflare 加密配置

原理: 这里的加密配置主要指的是 Cloudflare 请求我们的网站时是否需要加密(实际上这一步在开启小黄云时就可以设置),有 4 个选项:

加密模式选项

模式 用户 ↔ Cloudflare Cloudflare ↔ 源站 说明
完全严格(Full Strict) Cloudflare 证书加密 SSL 加密 + 证书验证 建议用这个,网站有证书就选它
完全(Full) Cloudflare 证书加密 SSL 加密,证书不验证 证书过期/不匹配也能通过
灵活(Flexible) Cloudflare 证书加密 HTTP 明文,无加密 不建议,不安全
关闭 无加密 无加密 全链路无加密

这里要根据服务器部署网站的方式来选择,我个人是选择第一种的,但我建议最起码都要选择第二种。


最后在 ITDOG 查看优选前后的测速状态:

未优选前的状态:

优选前测速

优选后的状态:

优选后测速


四、结语

本人作为一个非计算机从业者,上述的原理均为本人自身借助网站资料和 AI 学习理解,可能真实理解会有偏差,如有错误请纠正指出。

这篇文章,其实是用 Cloudflare 单域名优选的方式,抛砖引玉地说明了 CDN 和 Web 服务器是如何搭配使用的,这不仅仅是在网站 CDN 部署上,在其他可以发掘的场景也有借鉴之处,希望能帮到有需要的人,也是给我自己留下一个学习记录。

本文内容整理自 NodeSeek 社区,原文作者分享了 Cloudflare 单域名优选的完整实操流程与原理。如有疑问欢迎评论区交流。