nginx upstream server主动健康监测模块添加https检测功能

1 缘起

  前面的《nginx upstream server主动健康检测模块ngx_http_upstream_check_module 使用和源码分析》系列已经分析了ngx_http_upstream_check_module的实现原理,并且在借助这个模块的框架实现了一个udp健康检测的新功能。
  但是ngx_http_upstream_check_module还缺乏基于https监测上游服务器健康状况的能力,始终是一个缺憾,因此,本文基于《nginx upstream server主动健康检测模块ngx_http_upstream_check_module 使用和源码分析》《nginx stream proxy 模块的ssl连接源码分析》两篇博文的分析成果,来实现一个基于https的上游服务器健康检测的能力。

1.1 功能定义

  本次支持的功能:

  • 支持向上游服务器发起https请求功能
  • 请求的报文复用原有的http检测的请求报文定义
  • 响应的状态码检测复用原有的http检测的响应码的定义
  • 支持ssl握手过程中添加sni扩展信息
  • 支持ssl握手协议类型的配置
  • 支持ssl握手协议加密套件的配置

  暂时不支持的功能:

  • 不支持ssl会话复用(会话复用可以降低上游服务器的ssl握手压力)
  • 不支持ssl证书双向验证
  • 不支持服务器端证书有效性验证

&ems;  由于本次主要是检验https的链接握手流程,对一些不是特别关键的ssl握手特性暂时不支持主要是为了简化代码逻辑,但是不影响业务流程,这样也便于在本文中将整个实现流程进行阐述。后续可以参照ngx_stream_proxy_module中的实现,继续将这些特性进行完善,以臻于完美。

2. 实现后的效果

  首先来看一下实现后的效果,有一些感性的认识。

2.1 配置文件


#user  nobody;
worker_processes  1;
daemon off;
master_process off;

error_log  logs/error.log;
pid        logs/nginx.pid;

load_module  objs/ngx_http_upstream_check_module.so;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

	upstream backend {
		check type=https interval=3000 rise=2 fall=5 timeout=1000 port=443;
		check_http_send "GET / HTTP/1.1\r\nHost: www.test.com\r\n\r\n";
		check_http_expect_alive http_2xx http_3xx;
        check_ssl_server_name www.test.com;
		server 192.168.0.1:443;
	}

    server {
        listen       9080;
        server_name  localhost;

        # 开启本模块的状态查询接口	
		location /status {
			check_status html;
		}

        location / {
			proxy_pass http://backend;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

    }
}

   以上配置文件中的upstream块中定义了一个https的健康检测类型, check_http_send复用了http的定义,而check_ssl_server_name是新增的指令,用来配置ssl握手设置sni扩展主机host信息。

2.2 运行效果

查看nginx的error.log日志, 可以看到如下信息:

2024/02/16 09:31:33 [error] 23638#0: https check failed with return code: 403
2024/02/16 09:31:33 [error] 23638#0: check protocol https error with peer: 192.168.0.1:443
2024/02/16 09:31:45 [info] 23663#0: enable check peer: 192.168.0.1:443

error.log中的前面两条因为服务器响应403报了错误,对应在配置了“check_http_expect_alive http_2xx http_3xx;”的情况。如果配置改成“check_http_expect_alive http_2xx http_3xx http_4xx;” 则报后面那条上游服务器enable的信息。证明https的检测功能已经可用了。

3. 代码实现

3.1 配置指令

3.1.1 配置指令定义:

支持的配置指令如下:

  • check_ssl_ciphers:
        配置加密套件,格式参考proxy_ssl_ciphers
  • check_ssl_protocols:
        和服务器交互采用的ssl协议版本,如TLSv1.1 TLSv1.2等,格式参考proxy_ssl_protocols。
  • check_ssl_server_name:
        和服务器进行ssl握手时候采用的sni扩展host名字,如果不设置并且upstream块中的server是用域名设置的,那么默认就采用设置的服务器名字。
  • check_ssl_verify:[on/off]
        是否校验服务器的证书有效性。(待后续实现)
  • check_ssl_session_reuse: [on/off]
        和上游服务器进行ssl握手的时候是否复用ssl会话信息。

3.1.2 配置指令结构体:


struct ngx_http_upstream_check_srv_conf_s {
   
    ngx_uint_t                               port;
    ngx_uint_t                               fall_count;
    ngx_uint_t                               rise_count;
    ngx_msec_t                               check_interval;
    ngx_msec_t                               check_timeout;
    ngx_uint_t                               check_keepalive_requests;

    ngx_check_conf_t                        *check_type_conf;
    ngx_str_t                                send;

    union {
   
        ngx_uint_t                           return_code;
        ngx_uint_t                           status_alive;
    } code;

    ngx_array_t                             *fastcgi_params;

    ngx_uint_t                               default_down;
    ngx_uint_t                               unique;
    ngx_uint_t                               udp : 1;                       /* 是否udp socket */
    ngx_int_t                                match_part : 1;                /* 是否只要部分匹配就可以了 */
    ngx_int_t                                match_offset;                  /* udp响应期望的内容从哪个字节开始匹配 */
    ngx_str_t                                expect;                        /* udp响应的期望内容 */

#if (NGX_HTTP_SSL)
    ngx_ssl_t                               *ssl;                            /* ssl 配置上下文 */
    ngx_str_t                                ssl_ciphers;                    /* ssl 加密套件 */
    ngx_uint_t                               ssl_protocols;                  /* 采用的ssl协议 */
    ngx_str_t                                ssl_server_name;                /* ssl握手的sni扩展hostname */
#endif
};

  以上添加的ssl_protocols参数在ngx_http_upstream_check_create_srv_conf函数中需要将其设置为NGX_CONF_UNSET_UINT,避免nginx在解析配置文件的时候出现参数重复的报错。

3.1.3 配置指令源码定义:

#if (NGX_HTTP_SSL)
    {
    ngx_string("check_ssl_ciphers"),
      NGX_HTTP_UPS_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_SRV_CONF_OFFSET,
      offsetof(ngx_http_upstream_check_srv_conf_t, ssl_ciphers),
      NULL },

    {
    ngx_string("check_ssl_protocols"),
      NGX_HTTP_UPS_CONF|NGX_CONF_1MORE,
      ngx_conf_set_bitmask_slot,
      NGX_HTTP_SRV_CONF_OFFSET,
      offsetof(ngx_http_upstream_check_srv_conf_t, ssl_protocols),
      &ngx_upstream_check_ssl_protocols },

    {
    ngx_string("check_ssl_server_name"),
      NGX_HTTP_UPS_CONF|NGX_CONF_FLAG,
      ngx_conf_set_str_slot,
      NGX_HTTP_SRV_CONF_OFFSET,
      offsetof(ngx_http_upstream_check_srv_conf_t, ssl_server_name),
      NULL },
#endif

  通过以上配置指令,可以将解析到的参数设置到ngx_http_upstream_check_srv_conf_s结构体对应的字段中。其中ngx_upstream_check_ssl_protocols是一个可选协议的列表,定义如下:

#if (NGX_HTTP_SSL)

static ngx_conf_bitmask_t  ngx_upstream_check_ssl_protocols[] = {
   
    {
    ngx_string("SSLv2"), NGX_SSL_SSLv2 },
    {
    ngx_string("SSLv3"), NGX_SSL_SSLv3 },
    {
    ngx_string("TLSv1"), NGX_SSL_TLSv1 },
    {
    ngx_string("TLSv1.1"), NGX_SSL_TLSv1_1 },
    {
    ngx_string("TLSv1.2"), NGX_SSL_TLSv1_2 },
    {
    ngx_string("TLSv1.3"), NGX_SSL_TLSv1_3 },
    {
    ngx_null_string, 0 }
};

#endif

3.2 模块的初始化

static ngx_http_module_t  ngx_http_upstream_check_module_ctx = {
   
    NULL,                                    /* preconfiguration */
    ngx_http_upstream_check_init,            /* postconfiguration */

    ngx_http_upstream_check_create_main_conf,/* create main configuration */
    ngx_http_upstream_check_init_main_conf,  /* init main configuration */

    ngx_http_upstream_check_create_srv_conf, /* create server configuration */
    NULL,                                    /* merge server configuration */

    ngx_http_upstream_check_create_loc_conf, /* create location configuration */
    ngx_http_upstream_check_merge_loc_conf   /* merge location configuration */
};

  在以上代码中ngx_http_upstream_check_module_ctx结构体的初始化定义中,ngx_http_upstream_check_init_main_conf函数将在配置文件的http块解析完成后进行初始化,因此,我们通过修改这个函数来实现ssl上下文的初始化,即ngx_http_upstream_check_srv_conf_s的ssl指针的初始化。ssl是一个指向ngx_ssl_t的指针,用来对ssl协议的相关参数进行设定并初始化ssl上下文。
  以下是ngx_http_upstream_check_init_main_conf函数的代码:

static char *
ngx_http_upstream_check_init_main_conf(ngx_conf_t *cf, void *conf)
{
   
    ngx_buf_t                      *b;
    ngx_uint_t                      i;
    ngx_http_upstream_srv_conf_t  **uscfp;
    ngx_http_upstream_main_conf_t  *umcf;

    umcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_upstream_module);

    b = ngx_http_upstream_check_create_fastcgi_request(cf->pool,
            fastcgi_default_params,
            sizeof(fastcgi_default_params) / sizeof(ngx_str_t) / 2);

    if (b == NULL) {
   
        return NGX_CONF_ERROR;
    }

    fastcgi_default_request.data = b->pos;
    fastcgi_default_request.len = b->last - b->pos;

    uscfp = umcf->upstreams.elts;

    for (i = 0; i < umcf->upstreams.nelts; i++) {
   

        if (ngx_http_upstream_check_init_srv_conf(cf, uscfp[i]) != NGX_OK) {
   
            return NGX_CONF_ERROR;
        }
    }

    return ngx_http_upstream_check_init_shm(cf, conf);
}

  在以上代码中,

    for (i = 0; i < umcf->upstreams.nelts; i++) {
   

        if (ngx_http_upstream_check_init_srv_conf(cf, uscfp[i]) != NGX_OK) {
   
            return NGX_CONF_ERROR;
        }
    }

  这段代码是对所有配置的http upstream进行遍历,然后对它进行初始化,因为我们的ssl上下文信息每个upstream对应一个,因此,正好在ngx_http_upstream_check_init_srv_conf函数里面可以进行初始化。
  在函数ngx_http_upstream_check_init_srv_conf的最后返回前,我们添加ssl的初始化代码,如下:

static char *
ngx_http_upstream_check_init_srv_conf(ngx_conf_t *cf, void *conf)
{
   
......
#if (NGX_HTTP_SSL)
    /* 如果当前的监测类型是https才需要进行ssl的初始化设置 */
    if (check->type == NGX_HTTP_CHECK_HTTPS) {
   
        /* 如果ssl_protocols没有设置,
           那么默认开启NGX_SSL_TLSv1,NGX_SSL_TLSv1_1和NGX_SSL_TLSv1_2 
        */
        if (ucscf->ssl_protocols == NGX_CONF_UNSET_UINT) {
   
            ucscf->ssl_protocols = NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
                                    |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2;
        }
        
		/* 如果没有设置ssl_ciphers,那么设置默认值为DEFAULT */
        if (ucscf->ssl_ciphers.len == 0) {
   
            ngx_str_set(&ucscf->ssl_ciphers,"DEFAULT");
        }

        /* 为ssl分配内存 */
        ucscf->ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
        if (ucscf->ssl == NULL) {
   
            return NGX_CONF_ERROR;
        }

        ucscf->ssl->log = cf->log;

        /* 创建ssl上下文 */
        if (ngx_ssl_create(ucscf->ssl, ucscf->ssl_protocols, NULL) != NGX_OK) {
   
            return NGX_CONF_ERROR;
        }

		/* 设置ssl上下文回收回调函数 */
        cln = ngx_pool_cleanup_add(cf->pool, 0);
        if (cln == NULL) {
   
            return NGX_CONF_ERROR;
        }

        cln->handler = ngx_ssl_cleanup_ctx;
        cln->data = ucscf->ssl;

        /* 设置可以支持的加密套件 */
        if (ngx_ssl_ciphers(cf, ucscf->ssl, &ucscf->ssl_ciphers, 0) != NGX_OK) {
   
            return NGX_CONF_ERROR;
        }
    }

#endif
    return NGX_CONF_OK;
}

【未完待续】

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-02-17 07:32:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-02-17 07:32:02       100 阅读
  3. 在Django里面运行非项目文件

    2024-02-17 07:32:02       82 阅读
  4. Python语言-面向对象

    2024-02-17 07:32:02       91 阅读

热门阅读

  1. 2024-02-16 web3-区块链-keypass记录

    2024-02-17 07:32:02       55 阅读
  2. Docker之MongoDB安装、创建用户及登录认证

    2024-02-17 07:32:02       56 阅读
  3. 利用Excel模拟投币试验

    2024-02-17 07:32:02       51 阅读
  4. 指纹识别描述

    2024-02-17 07:32:02       62 阅读
  5. ubuntu20修改xorg.conf实现双屏幕输出

    2024-02-17 07:32:02       61 阅读
  6. 目标检测教程视频指南大全

    2024-02-17 07:32:02       52 阅读
  7. 收集域名信息

    2024-02-17 07:32:02       56 阅读
  8. 函数式编程要点

    2024-02-17 07:32:02       57 阅读