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 >= 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 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地层函数,该函数的定义如下:
__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 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连接请求的服务器有非常好的借鉴意义。
原创文章,作者:网络技术联盟站,如若转载,请注明出处:https://www.sudun.com/ask/49832.html