前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解nginx的https alpn机制

深入理解nginx的https alpn机制

作者头像
码农心语
发布2024-04-09 15:55:00
1110
发布2024-04-09 15:55:00
举报
文章被收录于专栏:码农心语码农心语

1. 概述

??应用层协议协商(Application-Layer Protocol Negotiation,简称ALPN)是一个传输层安全协议(TLS) 的扩展, ALPN 使得应用层可以协商在安全连接层之上使用什么协议, 避免了额外的往返通讯, 并且独立于应用层协议。ALPN 用于 HTTP/2 连接, 和HTTP/1.x 相比, HTTP 2的使用增强了网页的压缩率减少了网络延时。ALPN 和 HTTP/2 协议是伴随着 Google 开发 SPDY 协议出现的。

??nginx能够在一个ssl监听端口上同时提供http/1.1和http/2的服务,而http/2协议规定是必须基于tls安全通信协议的,因此,nginx在ssl握手过程中实现了ALPN的协议协商功能,能够自动完成和客户端的协议协商,从而根据客户端的协议支持能力提供http/1.1或者http/2的服务。

??本文基于nginx,对alpn的实现原理进行深入的分析。

2. alpn协议的简要理解

2.1 ssl的握手过程

?由上图可以看到,alpn的协商过程是在ssl握手的最早的两个阶段,即ClientHello和ServerHello中完成的,通过将应用层协议协商信息附加到ClientHello和ServerHello报文中完成的交互。

2.2 通过抓包看一下alpn的细节

??下面通过TLS v1.2握手协议来查看alpn的细节,对于TLS v1.3协议,在ServerHello响应的时候由于alpn部分的信息被加密,所以查看起来比较会麻烦。抓包通过wireshark来实现,通过以下命令来模拟http2的请求:

代码语言:javascript
复制
curl --http2 "https://www.test.com" -kv

??下到的报文如下:

??ClientHello报文:

??ServerHello报文:

??在ClientHello报文中可以看到application_layer_protocol_negotiation的信息,表明了客户端可以同时支持h2和http/1.1,而在ServerHello报文中也可以看到application_layer_protocol_negotiation的信息,表明服务器选择了h2协议作为应用层协议。

3. nginx源码分析

3.1 给ssl上下文设置alpn回调

?? nginx在启动的时候,ngx_http_ssl_module模块在ngx_http_ssl_merge_srv_conf的时候,有以下这段代码对ssl的上下文进行初始化:

代码语言:javascript
复制
/* 创建ssl上下文 */
  if (ngx_ssl_create(&conf->ssl, conf->protocols, conf) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

  /* 注册用于ssl上下文资源回收的回调函数
    cln = ngx_pool_cleanup_add(cf->pool, 0);
    if (cln == NULL) {
        ngx_ssl_cleanup_ctx(&conf->ssl);
        return NGX_CONF_ERROR;
    }

    cln->handler = ngx_ssl_cleanup_ctx;
    cln->data = &conf->ssl;

  /* 设置ClientHello消息回调 */
#if defined(T_INGRESS_SHARED_MEMORY_PB) && OPENSSL_VERSION_NUMBER >= 0x10101000L
    SSL_CTX_set_client_hello_cb(conf->ssl.ctx,
                                ngx_http_ssl_client_hello_callback, NULL);
#endif

  /* 设置SNI消息回调 */
#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME

    if (SSL_CTX_set_tlsext_servername_callback(conf->ssl.ctx,
                                               ngx_http_ssl_servername)
        == 0)
    {
        ngx_log_error(NGX_LOG_WARN, cf->log, 0,
            "nginx was built with SNI support, however, now it is linked "
            "dynamically to an OpenSSL library which has no tlsext support, "
            "therefore SNI is not available");
    }

#endif

  /* 设置ALPN消息回调  */
#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
    SSL_CTX_set_alpn_select_cb(conf->ssl.ctx, ngx_http_ssl_alpn_select, NULL);
#endif

?? 没错,最以上源码的最后部分,nginx向openssl底层库设置了alpn的回调函数ngx_http_ssl_alpn_select,以期待接收到从客户端发过来的ClientHello中分析出有alpn扩展信息的时候回调这个函数。

3.2 连接初始化

??在3.1节中所述的ssl上下文准备好以后,ssl连接当然是还没有建立的,只能说仍然只是停留在配置阶段,那么接下去可以想到客户端发起了tcp连接,nginx接受了这个连接,就需要开始对这个连接进行初始化,连接的初始化过程是由ngx_http_init_connection函数来完成的。那么如果开启了https,就会执行如下代码:

代码语言:javascript
复制
#if (NGX_HTTP_SSL)
    {
    ngx_http_ssl_srv_conf_t  *sscf;

    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);

    if (sscf->enable || hc->addr_conf->ssl) {
        hc->ssl = 1;
        c->log->action = "SSL handshaking";
        rev->handler = ngx_http_ssl_handshake;
    }
    }
#endif


??这段代码给当前连接的读事件设置了一个回调函数,即ngx_http_ssl_handshake函数,它用来进行ssl的握手操作。那么当nginx从这个连接上收到请求数据的时候就会开始执行ssl握手操作。在ngx_http_ssl_handshake函数中,有以下这段代码:

代码语言:javascript
复制
  if (ngx_ssl_create_connection(&sscf->ssl, c, NGX_SSL_BUFFER)
    != NGX_OK)
  {
    ngx_http_close_connection(c);
    return;
  }

??这段代码用之前启动阶段准备好的ssl上下文和当前的socket连接来创建一个新的ssl连接,这样子就将当前的socket连接和ssl上下文关联起来了。后面就是真正的ssl握手操作了,在ngx_http_ssl_handshake代码里有:

代码语言:javascript
复制
    rc = ngx_ssl_handshake(c);

??在ngx_ssl_handshake函数里面会发起异步的ssl握手操作,这里略过。

3.3 处理alpn协议回调

?? 在握手期间,ssl底层逻辑会解析ClientHello数据报文,发现有alpn数据后,就回调前面设置好的ngx_http_ssl_alpn_select函数了。下面来分析一下ngx_http_ssl_alpn_select函数的实现:

代码语言:javascript
复制
static int
ngx_http_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn, const unsigned char **out,
    unsigned char *outlen, const unsigned char *in, unsigned int inlen,
    void *arg)
{
    unsigned int            srvlen;
    unsigned char          *srv;
#if (NGX_DEBUG)
    unsigned int            i;
#endif

#if (NGX_HTTP_V2)
    ngx_http_connection_t  *hc;
#if (T_NGX_HTTP2_SRV_ENABLE)
    ngx_http_v2_srv_conf_t *h2scf;
#endif
#endif
#if (NGX_HTTP_V2 || NGX_DEBUG)
    ngx_connection_t       *c;

  /* 获取ssl连接的底层socket连接 */
    c = ngx_ssl_get_connection(ssl_conn);
#endif

#if (NGX_DEBUG)
    for (i = 0; i < inlen; i += in[i] + 1) {
        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "SSL ALPN supported by client: %*s",
                       (size_t) in[i], &in[i + 1]);
    }
#endif

#if (NGX_HTTP_V2)
    hc = c->data;

#if (T_NGX_HTTP2_SRV_ENABLE)
    h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);
#endif

    if (
#if (T_NGX_HTTP2_SRV_ENABLE)
        (
#endif
        hc->addr_conf->http2
#if (T_NGX_HTTP2_SRV_ENABLE)
        && h2scf->enable != 0) || h2scf->enable == 1
#endif
        )
    {
        /* 如果开启了http2,那么http2是优先协议排在前面,
           然后是http/1.1 http/1.0 http/0.9  
        */
        srv = (unsigned char *) NGX_HTTP_V2_ALPN_PROTO NGX_HTTP_ALPN_PROTOS;
        srvlen = sizeof(NGX_HTTP_V2_ALPN_PROTO NGX_HTTP_ALPN_PROTOS) - 1;
    } else
#endif
    {
        srv = (unsigned char *) NGX_HTTP_ALPN_PROTOS;
        srvlen = sizeof(NGX_HTTP_ALPN_PROTOS) - 1;
    }
    
  /* server端和client端支持的协议进行匹配,按server端支持列表顺序选择两者都支持的协议 */
    if (SSL_select_next_proto((unsigned char **) out, outlen, srv, srvlen,
                              in, inlen)
        != OPENSSL_NPN_NEGOTIATED)
    {
        return SSL_TLSEXT_ERR_ALERT_FATAL;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                   "SSL ALPN selected: %*s", (size_t) *outlen, *out);

    return SSL_TLSEXT_ERR_OK;
}

#endif

??本函数的入口参数in中存放的是alpn的协议名称列表,格式如下:

?1个或者多个alpn的协议名称被并排连续组装在了一起,长度字段占一个字节,长度字段表示了后面协议名占多少个字节。 ?i入口参数nlen表示了入口参数in指针指向的地址总共包含了多少字节的数据。

??接下去就是要做真正的协议选择了,协议选择最终是通过SSL_select_next_proto来完成了,这个是SSL地层函数,该函数的定义如下:

代码语言:javascript
复制
__owur int SSL_select_next_proto(unsigned char **out, unsigned char *outlen,
                                 const unsigned char *in, unsigned int inlen,
                                 const unsigned char *client,
                                 unsigned int client_len);

??其中out和outlen表示最终选择的协议名称及其长度,in和inlen表示服务器端的可选协议列表及其长度,client和client_len表示客户端的可选协议列表及其长度,在第一个in中设置的并且在client中存在的协议名称将被选中并输出到out和outlen中。 ?经过ngx_http_ssl_alpn_select的协议选择,ssl底层会把选择的结果保存起来。待ssl底层握手完成后,nginx需要根据握手的alpn结果设置是否启用http2。

3.4 握手完成,启用http协议

?? 经过3.3节的步骤,已经完成了协议的选择,那么接下去就是nginx的事情了,它需要根据选择的结果,是开启http2还是http1.1与客户端进行通信,当然接下去的通讯因为已经是ssl握手后了,所以数据的通讯都是经过ssl加密的了。 ?在ssl握手完成后,ssl底层将回调ngx_http_ssl_handshake_handler函数,这个函数是在函数ngx_http_ssl_handshake中通过以下代码设置的:

代码语言:javascript
复制
  rc = ngx_ssl_handshake(c);

  if (rc == NGX_AGAIN) {
    /* 如果异步握手没有即时完成,则设置ssl握手回调函数ngx_http_ssl_handshake_handler
    if (!rev->timer_set) {
      cscf = ngx_http_get_module_srv_conf(hc->conf_ctx,
                        ngx_http_core_module);
      ngx_add_timer(rev, cscf->client_header_timeout);
    }

    c->ssl->handler = ngx_http_ssl_handshake_handler;
    return;
  }
  /* 如果握手即时完成了,则直接调用ngx_http_ssl_handshake_handler*/
  ngx_http_ssl_handshake_handler(c);
br

?? 最后来看看ngx_http_ssl_handshake_handler函数的实现,源码如下:

代码语言:javascript
复制
static void
ngx_http_ssl_handshake_handler(ngx_connection_t *c)
{
    if (c->ssl->handshaked) {

        /*
         * The majority of browsers do not send the "close notify" alert.
         * Among them are MSIE, old Mozilla, Netscape 4, Konqueror,
         * and Links.  And what is more, MSIE ignores the server's alert.
         *
         * Opera and recent Mozilla send the alert.
         */

        c->ssl->no_wait_shutdown = 1;

#if (NGX_HTTP_V2                                                              \
     && defined TLSEXT_TYPE_application_layer_protocol_negotiation)
        {
        unsigned int            len;
        const unsigned char    *data;
        ngx_http_connection_t  *hc;

#if (T_NGX_HTTP2_SRV_ENABLE)
        ngx_http_v2_srv_conf_t *h2scf;
#endif
        hc = c->data;

#if (T_NGX_HTTP2_SRV_ENABLE)
        h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);
#endif

        if (
#if (T_NGX_HTTP2_SRV_ENABLE)
            (
#endif
             hc->addr_conf->http2
#if (T_NGX_HTTP2_SRV_ENABLE)
             && h2scf->enable != 0) || h2scf->enable == 1
#endif
           )
        {

#ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
      /* 获取alpn的选择结果 */
            SSL_get0_alpn_selected(c->ssl->connection, &data, &len);

#ifdef TLSEXT_TYPE_next_proto_neg
            if (len == 0) {
                SSL_get0_next_proto_negotiated(c->ssl->connection, &data, &len);
            }
#endif

#else /* TLSEXT_TYPE_next_proto_neg */
            SSL_get0_next_proto_negotiated(c->ssl->connection, &data, &len);
#endif
      /* 如果选择结果是 h2,那么就执行http2的初始化 */
            if (len == 2 && data[0] == 'h' && data[1] == '2') {
                ngx_http_v2_init(c->read);
                return;
            }
        }
        }
#endif

        c->log->action = "waiting for request";
    /* 设置连接读事件的回调函数ngx_http_wait_request_handler进行http/1.1的处理
    */
        c->read->handler = ngx_http_wait_request_handler;
        /* STUB: epoll edge */ c->write->handler = ngx_http_empty_handler;

        ngx_reusable_connection(c, 1);

        ngx_http_wait_request_handler(c->read);

        return;
    }

    if (c->read->timedout) {
        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
    }

    ngx_http_close_connection(c);
}

?? ngx_http_ssl_handshake_handler函数的实现和我们猜测的一样,就是从ssl底层通过SSL_get0_alpn_selected函数获取alpn的选择结果,如果没有获取到,则通过SSL_get0_next_proto_negotiated获取npn的选择结果。最后,发现如果选择的是h2(即http2),则开始初始化http2连接,否则设置连接的读事件回调为ngx_http_wait_request_handler,进入到http/1.1的后续处理阶段。

4.4 总结

??本文从ssl上下文的初始化、ssl连接的初始化、alpn回调处理,到最后ssl握手完成并启用http2协议的整个流程说明了nginx alpn的实现过程,nginx的实现逻辑清晰,简单明了,对我们未来自己去实现支持ssl连接请求的服务器有非常好的借鉴意义。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-29,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 码农心语 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 概述
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com