FPs

Nginx 加载配置的顺序分析

问题背景

在部署HTTPS 的时候,2台Nginx 下都部署了多个域名的证书,即NginxA 部署了证书a.com 和b.com 的证书,NginxB 也部署了证书a.com 和b.com ,域名a.com 指向NginxA,域名b.com 指向NginxB。
在用SSL LABS 检测的时候,发现b.com 会出现“This site works only in browsers with SNI support.”,而a.com 不会。
SNI

SNI 介绍

关于SNI 的简单介绍:

在 Nginx 中可以通过指定不同的 server_name 来配置多个站点。HTTP/1.1 协议请求头中的 Host 字段可以标识出当前请求属于哪个站点。但是对于 HTTPS 网站来说,要想发送 HTTP 数据,必须等待 SSL 握手完成,而在握手阶段服务端就必须提供网站证书。对于在同一个 IP 部署不同 HTTPS 站点,并且还使用了不同证书的情况下,服务端怎么知道该发送哪个证书?
Server Name Indication,简称为 SNI,是 TLS 的一个扩展,为解决这个问题应运而生。有了 SNI,服务端可以通过 Client Hello 中的 SNI 扩展拿到用户要访问网站的 Server Name,进而发送与之匹配的证书,顺利完成 SSL 握手。

引用来源:关于启用 HTTPS 的一些经验分享(二)

使用OpenSSL 测试NginxA,NginxB,不指定Host 头,NginxA 返回的是a.com 的证书,NginxB 返回的也是a.com 的证书。

openssl s_client -connect nginx_ip:443 -showcerts < /dev/null 

Nginx 源码分析

基本确定是配置加载的顺序问题。在NginxA 和NginxB 中先加载的都是a.com。 在nginx.conf 中,我用 include 指令 引入了 某目录下的所有conf 结尾的配置:

include /home/xxxxx/*.conf;

怀疑Nginx include 是按照字典顺序,即a-z 的顺序。看下源代码,参考的Nginx 代码版本是1.11.0。
先找到include 指定相关的函数:

ngx_conf_file.c

#/src/core/ngx_conf_file.c
static ngx_command_t  ngx_conf_commands[] = {

    { ngx_string("include"),
    NGX_ANY_CONF|NGX_CONF_TAKE1,
    ngx_conf_include,
    0,
    0,
    NULL },

    ngx_null_command
};

继续查看ngx_conf_include 函数,略去部分片段:

#/src/core/ngx_config_file.c

char *
ngx_conf_include(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ...
    ngx_glob_t   gl;

    ....

    gl.pattern = file.data;
    gl.log = cf->log;
    gl.test = 1;

    if (ngx_open_glob(&gl) != NGX_OK) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
        ngx_open_glob_n " \"%s\" failed", file.data);
        return NGX_CONF_ERROR;
    }

    rv = NGX_CONF_OK;

    for ( ;; ) {
        n = ngx_read_glob(&gl, &name);
        ...
    }

    ngx_close_glob(&gl);

    return rv;
}

ngx_open_glob 这个函数有点眼熟,glob 是一个熟悉的名字,在Python 和其他语言中常常看到相关的库,用来做文件路径搜索和匹配。继续找ngx_open_glob 这个函数:

#/src/os/unix/ngx_files.c

ngx_int_t
ngx_open_glob(ngx_glob_t *gl)
{
    int  n;

    n = glob((char *) gl->pattern, 0, NULL, &gl->pglob);

    if (n == 0) {
        return NGX_OK;
    }

#ifdef GLOB_NOMATCH

    if (n == GLOB_NOMATCH && gl->test) {
        return NGX_OK;
}

#endif

    return NGX_ERROR;
}

ngx_conf_include 在后续拿到匹配的文件路径,循环调用ngx_read_glob 进行读取解析。
ngx_open_glob 中调用了系统的glob.h 中的glob 函数进行路径匹配。

glob, globfree - find pathnames matching a pattern, free memory from glob()

#include <glob.h>

int glob(const char *pattern, int flags,
        int (*errfunc) (const char *epath, int eerrno),
        glob_t *pglob);
void globfree(glob_t *pglob);

继续搞清楚返回顺序的问题,glob 函数的传参第二个是flags。
flags 可以设置为GLOB_NOSORT,要求对返回的结果不进行排序,也就是说默认是排序的。

GLOB_NOSORT
Don't sort the returned pathnames.  The only reason to do this
is to save processing time.  By default, the returned
pathnames are sorted.

ngx_open_glob 中调用glob是传的flagsint 0。在GLOB 的手册中没找到关于flags 设置为0 的说明。继续翻代码。

参考glob(3)

glob.c

上github 搜了下posix 的glob 实现,

#https://github.com/lattera/glibc/blob/master/posix/glob.h

/* Bits set in the FLAGS argument to `glob'.  */
#define    GLOB_ERR    (1 << 0)/* Return on read errors.  */
#define    GLOB_MARK    (1 << 1)/* Append a slash to each name.  */
#define    GLOB_NOSORT    (1 << 2)/* Don't sort the names.  */
#define    GLOB_DOOFFS    (1 << 3)/* Insert PGLOB->gl_offs NULLs.  */
#define    GLOB_NOCHECK    (1 << 4)/* If nothing matches, return the pattern.  */
#define    GLOB_APPEND    (1 << 5)/* Append to results of a previous call.  */
#define    GLOB_NOESCAPE    (1 << 6)/* Backslashes don't quote metacharacters.  */
#define    GLOB_PERIOD    (1 << 7)/* Leading `.' can be matched by metachars.  */

GLOB 的手册中有一句话:

The argument flags is made up of the bitwise OR of zero or more the following symbolic constants.

也就是说可以flag 可以设置为多个,例如:GLOB_APPEND|GLOB_NOSORT

glob.c 中 不少if(!(flags & GLOB_DOOFFS)) 这样的语句,就明白了,通过与操作,可以在flags 这样一个变量上存储和提取多个组合配置。

#https://github.com/lattera/glibc/blob/master/posix/glob.c#L1243

if (!(flags & GLOB_NOSORT))
    {
        /* Sort the vector.  */
        qsort (&pglob->gl_pathv[oldcount],
              pglob->gl_pathc + pglob->gl_offs - oldcount,
              sizeof (char *), collated_compare);
    }

ngx_open_glob中的flags 是int 0,即(!(flags & GLOB_NOSORT))的结果为True,即默认进行排序,qsort 的比较函数是collated_compare:

#https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/posix/glob.c#L1283

static int
collated_compare (const void *a, const void *b)
{
    const char *const s1 = *(const char *const * const) a;
    const char *const s2 = *(const char *const * const) b;

    if (s1 == s2)
        return 0;
    if (s1 == NULL)
        return 1;
    if (s2 == NULL)
        return -1;
    return strcoll (s1, s2);
}

strcoll 函数会根据本地环境变量LC_COLLATE 的设置,来进行对比,细节不太清楚。不过一般的英文环境下,按照ASCII 表,如果s1 大于 s2,则返回值大于0,反之小于0,一样的话返回值为0。 collated_compare 函数比较结果中,如果小于0,qsort 会把第一个参数s1 排在s2 前面。在ASCII表中,0~9,a-Z的值是从小到大。

参考资料: C library function - strcoll()

所以,glob 函数在默认情况下,返回的结果是按照字母表排序的。即同时存在a.com.conf 和b.com.conf 两份配置,在同时include 的话,a.com.conf 会比b.com.conf 先加载。

最后,我把NginxA上的a.com.conf 改成 00-a.com.conf ,把NginxB 上的b.com.conf 改成 00-b.com.conf ,控制了他们的加载顺序。

题外话,想起了在用SySV Init 的时候,/etc/rcX.d 下那些 S01XXX,K02XXX 之类的文件,用来控制服务的起停顺序。

sysv_init_rcx.d

图片来源网络 ,现在手头已经找不到用SySV 做Init 的机器了,冏。

Changeset 4943:1e2d5d3f9f6b

Changeset 4943:1e2d5d3f9f6b in nginx

Message:
Core: removed GLOB_NOSORT glob option.
This will result in alphabetical sorting of included files if
the "include" directive with wildcards is used.

Note that the behaviour is now different from that on Windows, where
alphabetical sorting is not guaranteed for FindFirsFile?()/FindNextFile?()
(used to be alphabetical on NTFS, but not on FAT).

Approved by Igor Sysoev, prodded by many.
2016-10-14 nginx glob