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