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报文:

??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 >= 0x10101000LSSL_CTX_set_client_hello_cb(conf->ssl.ctx,ngx_http_ssl_client_hello_callback, NULL);#endif/* 设置SNI消息回调 */#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAMEif (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_negotiationSSL_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);#endifif (#if (T_NGX_HTTP2_SRV_ENABLE)(#endifhc->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_handlerif (!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;#endifhc = c->data;#if (T_NGX_HTTP2_SRV_ENABLE)h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);#endifif (#if (T_NGX_HTTP2_SRV_ENABLE)(#endifhc->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_negif (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;}}}#endifc->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