深入理解nginx的https alpn机制

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的请求:

curl --http2 \\\"https://www.test.com\\\" -kv

??下到的报文如下:

??ClientHello报文:

深入理解nginx的https alpn机制

??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的上下文进行初始化:

/* 创建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,就会执行如下代码:

#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函数中,有以下这段代码:

  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代码里有:

    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函数的实现:

static intngx_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地层函数,该函数的定义如下:

__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中通过以下代码设置的:

  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函数的实现,源码如下:

static voidngx_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连接请求的服务器有非常好的借鉴意义。

原创文章,作者:网络技术联盟站,如若转载,请注明出处:https://www.sudun.com/ask/49832.html

Like (0)
网络技术联盟站的头像网络技术联盟站
Previous 2024年5月19日
Next 2024年5月19日

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注