Nginx中server_name的一些无用冷知识


背景

在早前 初识Nginx中server和location的匹配顺序 中有简单提到 server_name 的匹配顺序。然而,在日常的使用过程中不免会遇到一些”坑”,在不断踩坑的过程中加深了对现有知识的理解,并借此机会进行了补充。


关于 server_name 的匹配顺序

以下内容其实在 How nginx processes a request 也有说明,下文主要是基于日常配置过程中加以补充。

  1. listen 匹配(作为大前提,如果访问端口都不对,就不会进行后续的域名匹配)
  2. 精确域名匹配
  3. * 开头,最长匹配的泛域名匹配,如: *.test.com
  4. * 结尾,最长匹配的泛域名匹配,如: www.test.*
  5. 正则域名匹配,如: ~^\.www\.test\.com$
  6. 如果都不匹配
    1. 优先选择 listen 配置项后有 default 或 default_server 的 server 块匹配
    2. 根据 server_name localhost 匹配 (当且仅当请求 Host 为 localhost 且 server_name 也为 localhost 时才生效)
    3. 根据 Nginx 配置文件/目录中, ls -l 结果从上至下(按 0-9 -> a-z)顺序匹配,匹配到的第一个符合条件的即止
      • 例如: 有 80 监听的第一个 server 块,忽略 server_name,直接匹配对应 location

注意事项

  • 在条件允许的情况下,请尽量使用精确域名
  • 当出现多个相同的 server_name 时,会根据站点配置文件排序优先匹配;
  • 在普通 server_name 配置下,* 符号可以代表多字符匹配,但只能用在 server_name 的开头或末尾
    • 例如: www.*.example.orgw*.example.org 是无效的
  • Nginx 的正则表达式与 Perl 语言(PCRE)使用的正则表达式兼容;
  • 正则匹配必须以 ~ 开头(否则将会认为是精确字符匹配),并建议使用 ^$ 符号分别作为”开头”和”结束”的锚点;
    • 例如: server_name ~^www\d+\.example\.net$;
  • 正则域名下允许在 server_name 中使用 * 符号匹配多字符;
    • 例如: ~^w.*\.example\.org$ 不仅匹配 www.example.org,还匹配 www.sub.example.org

关于泛域名匹配

泛域名配置

根据 Server names#wildcard_names 中说明,在普通泛域名配置中,* 符号只能对域名”前缀”或”后缀”进行配置,其他位置则不生效。

server {
    listen       80;
    server_name  *.example.org;
    ...
}

server {
    listen       80;
    server_name  mail.*;
    ...
}

正则泛域名配置

如果想对固定域名的部分字符进行泛域名匹配时(例如匹配: api.dev.nestealin.comapi.pre.nestealin.comapi.prod.nestealin.comapi.*.nestealin.com 格式的域名),则可以使用正则表达式的方式进行。

参考: Server names#regex_names

server {
    listen       80;
    server_name  ~^api\.*\.nestealin\.com$;
    ...
}

泛域名误区 - “无意义域名”

在一些教程网站中,也有一些配置会用 server_name ""server_name _ 来表示”泛域名”匹配,但这其实并不完全符合匹配原则,只是”恰好”在规则条件上得以命中,看似解决了这方面问题罢了。下面将针对最常见的几个”泛域名” server_name 配置进行主要说明。

server_name “”

Nginx 的官方文档是这么描述 server_name "" 的:

If it is required to process requests without the “Host” header field in a server block which is not the default, an empty name should be specified.

也就是说,如果想让一个没有被标注为 default_server 的 server 块来处理一个没有带有 Host header 的请求的话,就需要使用 server_name "" 指令。但问题是,在 RFC 7230, Section 5.4 中 HTTP/1.1 (也就是当前主流的 HTTP version)对 Host 的规定[^3],如果一个请求没有带有 Host header 的话,服务器必须得返回 400(Bad Request):

所以,使用了 server_name ""非 default server 块压根就没有机会去处理一个没有带有 Host header 的请求,因为在把这个请求交给 server 块之前就已经先被 Nginx 处理然后响应 400 状态码了。(即便客户端为 HTTP/1.0 结果也是同样)

因此,当 server_name "" 单独使用时,该 server 块是处理不了任何请求的(按正常匹配顺序来说),它通常要跟 listen 中的 defaultdefault_server 配合使用,表示这个 server 块可以集中处理那些无法被其他 server 块处理的请求。

server_name _ | server_name $ | server_name @

server_name _ 是可以匹配到 Host header 的值的,因为它可以匹配到 Host header 值为 “_“ 的请求。但是这种匹配其实也是没有意义的,因为对于浏览器而言,请求中的 Host header 的值就是你所输入的网站地址(可以是 IP 地址也可以是域名),但是 “_“ 根本就不是一个域名,它根本就无法被解析成 ip 地址,无法被解析成 ip 地址就意味着这个请求根本就没办法发送到对应的服务器上,所以 server_name _server_name "" 一样根本就没有用武之地。

同样地,我们可以认为 server_name _server_name "" 一样本身是没有任何意义;其实除了 server_name _ 之外,像 server_name !server_name $server_name @server_name -- 等这类都是属于”无意义域名”。

因此,server_name _ 其实跟 server_name "" 一样,在 server_name _ 单独使用的 server 块中是处理不了任何请求的,一般也是要跟 listen 中的 defaultdefault_server 一起配合使用才有最佳效果,也是表示这个 server 块可以集中处理那些无法被其他 server 块处理的请求。


关于国际化域名

什么是国际化域名

在全球域名规范中,除了英文点分域名格式以外,还可能存在中文、俄文等国家文字作为域名,统称为国际化域名( Internationalized Domain Names (IDNs) )[^1][^2]。

如何配置国际化域名

假设现在要为一个俄文域名(пример.испытание)进行配置,此时就需要将域名转为 ASCII (Punycode) 格式。

例如:

server {
    listen       80;
    server_name  xn--e1afmkfd.xn--80akhbyknj4f;  # 实际域名: пример.испытание
    ...
}

Punycode 转换

在线转换工具: Punycode编码转换


关于匹配规则的实验

环境说明

服务端(Nginx)

Nginx IP: 192.168.7.27

在 Nginx 配置目录(/usr/local/nginx/conf/sites)下创建两个配置文件:

注: 为了避免”兜底配置”在站点文件的首位,因此,在本次实验中会将”兜底配置”文件根据匹配规则放到目录”末尾”。

正常域名配置: (a-test.conf)

server {
    listen 80;
    server_name abc.nestealin.com;
    location / {
        add_header content-type application/json;
        return 200 '{"status": "This is the a-test.conf"}';
    }
}

兜底域名配置: (z-default_test.conf)

server {
    #listen 80 default_server ;
    listen 80 ;
    #server_name "";
    server_name _;
    #server_name $;
    #server_name localhost;
    #server_name ~^(.+)$;
    #server_name ~^.*$;
    #server_name ~. "";
    location / {
        add_header content-type application/json;
        return 200 '{"status": "This is the z-default_test.conf"}';
    }
}

客户端

使用 curl 命令进行请求测试:

  • curl 版本: 8.4.0
  • 请求协议版本: HTTP/1.1

验证方式

在”正常域名”配置不变的情况下,不断修改兜底域名配置,客户端重复请求进行验证。

实验过程

server_name “” 并且是 default_server

客户端请求携带 Host

# curl http://192.168.7.27 -v
*   Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host: 192.168.7.27
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx
< Date: Fri, 05 Apr 2024 07:47:51 GMT
< Content-Type: application/octet-stream
< Content-Length: 37
< Connection: close
< content-type: application/json
<
* Closing connection
{"status": "This is the a-test.conf"}

客户端请求不携带 Host

# curl 192.168.7.27 -v -H "Host: "
*   Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host:
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Fri, 05 Apr 2024 03:51:50 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 150
< Connection: close
<
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Closing connection

server_name “” 但不是 default_server

Nginx 兜底域名配置改为如下

server {
    listen 80;
    server_name "";
    location / {
        add_header content-type application/json;
        return 200 '{"status": "ok"}';
    }
}

客户端请求携带 Host

# curl 192.168.7.27 -v
*   Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host: 192.168.7.27
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx
< Date: Fri, 05 Apr 2024 08:16:27 GMT
< Content-Type: application/octet-stream
< Content-Length: 45
< Connection: close
< content-type: application/json
<
* Closing connection
{"status": "This is the z-default_test.conf"}

客户端请求不携带 Host

# curl http://192.168.7.27 -v -H "Host: "
*   Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host:
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Fri, 05 Apr 2024 03:49:14 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 150
< Connection: close
<
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Closing connection

实验结果

由于繁复性结果相近,所以省略了其他过程内容,以下是整体结果:

当兜底配置的 listen 仅为 80 时

即类似如下配置:

server{
    listen 80;
    server_name XXXX;
    ...
}

注:

  • 表格首列为客户端的请求 Host;
  • 表格首行为 Nginx 站点配置中的 server_name 配置;
  • 表格内容为 Nginx 的响应状态码/请求命中的站点配置;
访问Host server_name "" server_name _ server_name localhost server_name ~^.*$
Null(空) 400 400 400 400
localhost a-test.conf a-test.conf z-default_test.conf z-default_test.conf
abc.nestealin.com a-test.conf a-test.conf a-test.conf a-test.conf
a1b2c3.nestealin.com a-test.conf a-test.conf a-test.conf z-default_test.conf
192.168.1.1 a-test.conf a-test.conf a-test.conf z-default_test.conf
192.168.7.27 a-test.conf a-test.conf a-test.conf z-default_test.conf
127.0.0.1 a-test.conf a-test.conf a-test.conf z-default_test.conf

当兜底配置的 listen 为 80 default_server 时

即类似如下配置:

server{
    listen 80 default_server;
    server_name XXXX;
    ...
}

注:

  • 表格首列为客户端的请求 Host;
  • 表格首行为 Nginx 站点配置中的 server_name 配置;
  • 表格内容为 Nginx 的响应状态码/请求命中的站点配置;
访问Host server_name "" server_name _ server_name localhost server_name ~^.*$
Null(空) 400 400 400 400
localhost z-default_test.conf z-default_test.conf z-default_test.conf z-default_test.conf
abc.nestealin.com a-test.conf a-test.conf a-test.conf a-test.conf
a1b2c3.nestealin.com z-default_test.conf z-default_test.conf z-default_test.conf z-default_test.conf
192.168.1.1 z-default_test.conf z-default_test.conf z-default_test.conf z-default_test.conf
192.168.7.27 z-default_test.conf z-default_test.conf z-default_test.conf z-default_test.conf
127.0.0.1 z-default_test.conf z-default_test.conf z-default_test.conf z-default_test.conf

实验小结

  • 无论是否存在默认监听(listen ${Port} default_server;),当请求 Host 为空时,响应结果均为 400;
  • 当没有默认监听且请求 Host 为 localhost 时,如果存在 server_name localhost 的话,在 Nginx 看来该 server_name 也是一个”精确域名”,可以命中对应 server 配置;
  • 当没有默认监听且请求 Host 不为空时,server_name 匹配顺序: 精准域名 > 泛域名 > 正则域名 > localhost (如果恰好有 Host 也为 localhost 的话) > 站点(配置文件)顺序;即”无意义域名”(server_name _ 等)并非真正意义上的”兜底域名”;
  • 当存在默认监听且请求 Host 不为空时,”无意义域名”、”localhost”、”~^.*$“配置等价,都能作为”兜底域名”存在,实现拦截对应 listen 端口中所有 server 配置外的 HTTP 请求;

总结

  • 在条件允许的情况下,应尽量使用精确域名,其次泛域名,最后是正则域名,请尽量少用无意义域名;
  • 在使用正则域名时,必须以 ~ 开头(否则将会认为是精确字符匹配),并建议使用 ^$ 符号分别作为”开头”和”结束”的锚点;
  • 在 listen 指令中,defaultdefault_server 作用相同,在 0.8.21 及以后的 Nginx 版本中,推荐使用 default_server 更符合官方规范;
  • 当请求 Host 为空时,Nginx 会遵循 RFC 7230, Section 5.4 中的规定[^3]直接响应 400 状态码,无视所有匹配规则;
  • server_name "" 等无意义域名期望作为”兜底配置”存在时,其 listen 指令必须要有 default_server 参数,否则可能会因匹配顺序而无法实现相应兜底逻辑;
  • 当没有默认监听且请求 Host 为 localhost 时,如果存在 server_name localhost 的配置,则该 server_name 也是一个”精确域名”,可以命中对应 server 配置;

最佳实践

1. 匹配 80 端口的所有 HTTP 请求 (兜底)

写法一: 正则域名

server {
	listen       80 default_server;
	server_name  ~^.*$;
	access_log logs/default.access.log main;
	...
}

通常,这种配置会作为兜底模块,其 listen 也建议设置成 default_server 进行兜底拦截,实现捕获所有规则外的请求。

注: 在 Nginx 0.8.21 之前的版本里,需要”默认监听”使用”default“进行声明;从 0.8.21 开始的版本新加入了”default_server“参数,其作用与”default“等价并兼容旧版本写法,可以选择任一声明。

写法二: 无意义域名

server {
	listen       80 default_server;
	server_name  _;
	access_log logs/default.access.log main;
	...
}

虽然不推荐这种方式,但其匹配顺序可以作为正则域名后的补充,可以提供参考。(例如: 存在大量正则域名,怕影响现存正则写法的情况。)

注意: 该配置的 default_server 不能去除,否则可能导致配置无效。

2. 匹配 80 端口所有 HTTP 请求并响应断开连接 (兜底)

如果 Nginx 对客户端请求有要求,客户端必须携带合规的 “Host” 标头字段才能访问(即自己没有配置的 server_name 都不允许访问),则可以定义一个 server 块来直接丢弃非法请求:

注: “Host”字段可以是 IP 地址,也可以是域名。

写法一: 正则域名

server {
	listen       80 default_server;
	server_name  ~^.*$;
	access_log logs/default.access.log main;
	return      444;
}

写法二: 无意义域名

server {
	listen       80 default_server;
	server_name  _;
	access_log logs/default.access.log main;
	return      444;
}

References

[^1]: Internationalized Domain Name - ICANNWIki
[^2]: Internationalized domain name - Wikipedia
[^3]: RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing


文章作者: NesTeaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesTeaLin !
  目录