一、什麼是SNI?
SNI是Server Name Indication的縮寫,是爲了解決一個服務器使用多個域名和證書的SSL/TLS擴展。它允許客戶端在發起SSL握手請求時(客戶端發出ClientHello消息中)提交請求的HostName信息,使得服務器能夠切換到正確的域並返回相應的證書。在SNI出現之前,HostName信息只存在於HTTP請求中,但SSL/TLS層無法獲知這一信息。通過將HostName的信息加入到SNI擴展中,SSL/TLS允許服務器使用一個IP爲不同的域名提供不同的證書,從而能夠與使用同一個IP的多個“虛擬主機”更方便地建立安全連接。
二、RFC中SNI的定義
RFC 6066《Transport Layer Security (TLS) Extensions: Extension Definitions》對SNI擴展做了詳細的定義。要點如下:1) Client需要在ClientHello中包含一個名爲”server_name”的擴展,這個擴展的”extension_data”域中需要包含”ServerNameList”;
2) ServerNameList中包含了多個HostName及其類型(name_type),但所有HostName的name_type不能相同(早期的RFC規範允許一個name_type多個HostName,但 實際上當前的client實現只發送一個HostName,而且client不一定知道server選擇了哪個HostName,因此禁止一個name_type多個HostName);
3) HostName中包含的是server的完全合格的DNS主機名,且HostName中不允許包含IPv4或IPv6地址;
4) 如果server收到的ClientHello中帶有”server_name”擴展,它也應該在ServerHello中包含一個”server_name”擴展,其中的”extension_data”域應爲空;
5) 當執行會話恢復時,clinet應該在ClientHello中包含與上次會話相同的”server_name”擴展。如果擴展中包含的name與上次的不同,server必須拒絕恢復會話。恢復會話時server必須不能在ServerHello中包含”server_name”擴展;
6) 如果一個應用程序使用應用層協議協商了一個server name然後升級到TLS,並且發送了”server_name”擴展,這個擴展中必須包含與在應用層協議中所協商的相同的server name。如果這個server name成功應用在了TLS會話中,client不應該在應用層嘗試請求一個不同的server name。
三、OpenSSL(基於OpenSSL-1.1.0f)與SNI
3.1 Client設置server_name擴展
用戶可以通過SSL_set_tlsext_host_name(s,name)函數來設置ClientHello中的Server Name:246 # define SSL_set_tlsext_host_name(s,name) \
247 SSL_ctrl(s,SSL_CTRL_SET_TLSEXT_HOSTNAME,TLSEXT_NAMETYPE_host_name,(char *)name)
1670 long SSL_ctrl(SSL *s, int cmd, long larg, void *parg)
1671 {
1672 long l;
1673
1674 switch (cmd) {
…
1747 default:
1748 return (s->method->ssl_ctrl(s, cmd, larg, parg));
1749 }
1750 }
對於TLS_client_method()和TLS_server_method(),s->method->ssl_ctrl指向ssl3_ctrl:2883 long ssl3_ctrl(SSL *s, int cmd, long larg, void *parg)
2884 {
2885 int ret = 0;
2886
2887 switch (cmd) {
…
2961 case SSL_CTRL_SET_TLSEXT_HOSTNAME:
2962 if (larg == TLSEXT_NAMETYPE_host_name) {
2963 size_t len;
2964
2965 OPENSSL_free(s->tlsext_hostname);
2966 s->tlsext_hostname = NULL;
2967
2968 ret = 1;
2969 if (parg == NULL)
2970 break;
2971 len = strlen((char *)parg);
2972 if (len == 0 || len > TLSEXT_MAXLEN_host_name) {
2973 SSLerr(SSL_F_SSL3_CTRL, SSL_R_SSL3_EXT_INVALID_SERVERNAME);
2974 return 0;
2975 }
2976 if ((s->tlsext_hostname = OPENSSL_strdup((char *)parg)) == NULL) {
2977 SSLerr(SSL_F_SSL3_CTRL, ERR_R_INTERNAL_ERROR);
2978 return 0;
2979 }
2980 } else {
2981 SSLerr(SSL_F_SSL3_CTRL, SSL_R_SSL3_EXT_INVALID_SERVERNAME_TYPE);
2982 return 0;
2983 }
2984 break;
…
可見SSL_set_tlsext_host_name(s,name)函數最終將name保存到s->tlsext_hostname上,在構建ClientHello擴展時將其發送出去:968 unsigned char *ssl_add_clienthello_tlsext(SSL *s, unsigned char *buf,
969 unsigned char *limit, int *al)
970 {
…
1027 if (s->tlsext_hostname != NULL) {
1028 /* Add TLS extension servername to the Client Hello message */
1029 size_t size_str;
1030
1031 /*-
1032 * check for enough space.
1033 * 4 for the servername type and extension length
1034 * 2 for servernamelist length
1035 * 1 for the hostname type
1036 * 2 for hostname length
1037 * + hostname length
1038 */
1039 size_str = strlen(s->tlsext_hostname);
1040 if (CHECKLEN(ret, 9 + size_str, limit))
1041 return NULL;
1042
1043 /* extension type and length */
1044 s2n(TLSEXT_TYPE_server_name, ret);
1045 s2n(size_str + 5, ret);
1046
1047 /* length of servername list */
1048 s2n(size_str + 3, ret);
1049
1050 /* hostname type, length and hostname */
1051 *(ret++) = (unsigned char)TLSEXT_NAMETYPE_host_name;
1052 s2n(size_str, ret);
1053 memcpy(ret, s->tlsext_hostname, size_str);
1054 ret += size_str;
1055 }
…
發送的ClientHello中的擴展信息如圖:
3.2 Server設置servername_callback
爲了根據ClientHello中的servername返回相應的證書以及進行其它相關處理,server需要設置callback函數來完成這些功能:279 # define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \
280 SSL_CTX_callback_ctrl(ctx,SSL_CTRL_SET_TLSEXT_SERVERNAME_CB,(void (*)(void))cb)
1882 long SSL_CTX_callback_ctrl(SSL_CTX *ctx, int cmd, void (*fp) (void))
1883 {
1884 switch (cmd) {
1885 case SSL_CTRL_SET_MSG_CALLBACK:
1886 ctx->msg_callback = (void (*)
1887 (int write_p, int version, int content_type,
1888 const void *buf, size_t len, SSL *ssl,
1889 void *arg))(fp);
1890 return 1;
1891
1892 default:
1893 return (ctx->method->ssl_ctx_callback_ctrl(ctx, cmd, fp));
1894 }
1895 }
3488 long ssl3_ctx_callback_ctrl(SSL_CTX *ctx, int cmd, void (*fp) (void))
3489 {
3490 switch (cmd) {
…
3498 case SSL_CTRL_SET_TLSEXT_SERVERNAME_CB:
3499 ctx->tlsext_servername_callback = (int (*)(SSL *, int *, void *))fp;
3500 break;
…
這裏設置的callback函數會在Server處理server_name擴展時調用。通常server在回調函數被調用時都需要知道ClientHello中包含的Host Name的內容,這可以通過SSL_get_servername()函數來實現:
2081 const char *SSL_get_servername(const SSL *s, const int type)
2082 {
2083 if (type != TLSEXT_NAMETYPE_host_name)
2084 return NULL;
2085
2086 return s->session && !s->tlsext_hostname ?
2087 s->session->tlsext_hostname : s->tlsext_hostname;
2088 }
3.3 Server處理server_name擴展
Server在檢查ClientHello時,如果發現了”server_name”擴展則會對其進行解析:1890 static int ssl_scan_clienthello_tlsext(SSL *s, PACKET *pkt, int *al)
1891 {
…
1986 else if (type == TLSEXT_TYPE_server_name) {
1987 unsigned int servname_type;
1988 PACKET sni, hostname;
1989
1990 if (!PACKET_as_length_prefixed_2(&extension, &sni)
1991 /* ServerNameList must be at least 1 byte long. */
1992 || PACKET_remaining(&sni) == 0) {
1993 return 0;
1994 }
1995
1996 /*
1997 * Although the server_name extension was intended to be
1998 * extensible to new name types, RFC 4366 defined the
1999 * syntax inextensibility and OpenSSL 1.0.x parses it as
2000 * such.
2001 * RFC 6066 corrected the mistake but adding new name types
2002 * is nevertheless no longer feasible, so act as if no other
2003 * SNI types can exist, to simplify parsing.
2004 *
2005 * Also note that the RFC permits only one SNI value per type,
2006 * i.e., we can only have a single hostname.
2007 */
2008 if (!PACKET_get_1(&sni, &servname_type)
2009 || servname_type != TLSEXT_NAMETYPE_host_name
2010 || !PACKET_as_length_prefixed_2(&sni, &hostname)) {
2011 return 0;
2012 }
2013
2014 if (!s->hit) {
2015 if (PACKET_remaining(&hostname) > TLSEXT_MAXLEN_host_name) {
2016 *al = TLS1_AD_UNRECOGNIZED_NAME;
2017 return 0;
2018 }
2019
2020 if (PACKET_contains_zero_byte(&hostname)) {
2021 *al = TLS1_AD_UNRECOGNIZED_NAME;
2022 return 0;
2023 }
2024
2025 if (!PACKET_strndup(&hostname, &s->session->tlsext_hostname)) {
2026 *al = TLS1_AD_INTERNAL_ERROR;
2027 return 0;
2028 }
2029
2030 s->servername_done = 1;
2031 } else {
2032 /*
2033 * TODO(openssl-team): if the SNI doesn't match, we MUST
2034 * fall back to a full handshake.
2035 */
2036 s->servername_done = s->session->tlsext_hostname
2037 && PACKET_equal(&hostname, s->session->tlsext_hostname,
2038 strlen(s->session->tlsext_hostname));
2039 }
2040 }
…
2009行:OpenSSL中的Server Name Type只有TLSEXT_NAMETYPE_host_name一種。2014-2039:如果不是出於會話恢復過程中,則將hostname解析到s->session->tlsext_hostname;否則對比擴展中的hostname和上次保存的hostname,不一致則發起全新的握手(不允許恢復會話)。
在解析完server_name擴展之後,OpenSSL會在ssl_check_clienthello_tlsext_early()函數中調用server設置的回調函數:
2319 int ssl_parse_clienthello_tlsext(SSL *s, PACKET *pkt)
2320 {
2321 int al = -1;
2322 custom_ext_init(&s->cert->srv_ext);
2323 if (ssl_scan_clienthello_tlsext(s, pkt, &al) <= 0) {
2324 ssl3_send_alert(s, SSL3_AL_FATAL, al);
2325 return 0;
2326 }
2327 if (ssl_check_clienthello_tlsext_early(s) <= 0) {
2328 SSLerr(SSL_F_SSL_PARSE_CLIENTHELLO_TLSEXT, SSL_R_CLIENTHELLO_TLSEXT);
2329 return 0;
2330 }
2331 return 1;
2332 }
2670 static int ssl_check_clienthello_tlsext_early(SSL *s)
2671 {
2672 int ret = SSL_TLSEXT_ERR_NOACK;
2673 int al = SSL_AD_UNRECOGNIZED_NAME;
2674
2675 #ifndef OPENSSL_NO_EC
2676 /*
2677 * The handling of the ECPointFormats extension is done elsewhere, namely
2678 * in ssl3_choose_cipher in s3_lib.c.
2679 */
2680 /*
2681 * The handling of the EllipticCurves extension is done elsewhere, namely
2682 * in ssl3_choose_cipher in s3_lib.c.
2683 */
2684 #endif
2685
2686 if (s->ctx != NULL && s->ctx->tlsext_servername_callback != 0)
2687 ret =
2688 s->ctx->tlsext_servername_callback(s, &al,
2689 s->ctx->tlsext_servername_arg);
2690 else if (s->session_ctx != NULL
2691 && s->session_ctx->tlsext_servername_callback != 0)
2692 ret =
2693 s->session_ctx->tlsext_servername_callback(s, &al,
2694 s->
2695 session_ctx->tlsext_servername_arg);
2696
2697 switch (ret) {
2698 case SSL_TLSEXT_ERR_ALERT_FATAL:
2699 ssl3_send_alert(s, SSL3_AL_FATAL, al);
2700 return -1;
2701
2702 case SSL_TLSEXT_ERR_ALERT_WARNING:
2703 ssl3_send_alert(s, SSL3_AL_WARNING, al);
2704 return 1;
2705
2706 case SSL_TLSEXT_ERR_NOACK:
2707 s->servername_done = 0;
2708 default:
2709 return 1;
2710 }
2711 }
如果call_back函數返回的是SSL_TLSEXT_ERR_OK,則OpenSSL會在ServerHello中添加一個內容爲空的server_name擴展:1449 unsigned char *ssl_add_serverhello_tlsext(SSL *s, unsigned char *buf,
1450 unsigned char *limit, int *al)
1451 {
…
1500 if (!s->hit && s->servername_done == 1
1501 && s->session->tlsext_hostname != NULL) {
1502 /*-
1503 * check for enough space.
1504 * 4 bytes for the server name type and extension length
1505 */
1506 if (CHECKLEN(ret, 4, limit))
1507 return NULL;
1508
1509 s2n(TLSEXT_TYPE_server_name, ret);
1510 s2n(0, ret);
1511 }
…
發送的樣式如下圖所示:3.4 Client處理server_name擴展
Client在處理ServerHello中如果發現了server_name擴展,則會檢查自己之前是否發送過這個擴展。如果發送過則將擴展的名字記錄在會話中:2354 static int ssl_scan_serverhello_tlsext(SSL *s, PACKET *pkt, int *al)
2355 {
2356 unsigned int length, type, size;
2357 int tlsext_servername = 0;
…
2405 } else if (type == TLSEXT_TYPE_server_name) {
2406 if (s->tlsext_hostname == NULL || size > 0) {
2407 *al = TLS1_AD_UNRECOGNIZED_NAME;
2408 return 0;
2409 }
2410 tlsext_servername = 1;
2411 }
…
2610 if (!s->hit && tlsext_servername == 1) {
2611 if (s->tlsext_hostname) {
2612 if (s->session->tlsext_hostname == NULL) {
2613 s->session->tlsext_hostname =
2614 OPENSSL_strdup(s->tlsext_hostname);
2615 if (!s->session->tlsext_hostname) {
2616 *al = SSL_AD_UNRECOGNIZED_NAME;
2617 return 0;
2618 }
2619 } else {
2620 *al = SSL_AD_DECODE_ERROR;
2621 return 0;
2622 }
2623 }
2624 }
…
3.5 總結
綜上,對於OpenSSL的SNI擴展,client端的處理主要是通過SSL_set_tlsext_host_name(s, name)函數將Host Name的信息添加到Client擴展中,這樣就可以在SSL/TLS層使得server或者Host Name信息。
Server對SNI功能的支持就要複雜一些:它需要註冊一個回調函數,在回調函數中調用SSL_get_servername()函數獲取Host Name,再處理Host Name與證書的綁定關係等。這個部分是SNI的核心功能,需要由OpenSSL的用戶來實現。
此外,OpenSSL還會在會話恢復過程中檢查Host Name的一致性,如果不一致則不允許恢復會話,必須重新發起handshake。
四、Nginx與SNI
4.1 檢查SNI功能
執行nginx –V。如果輸出的信息中有“TLS SNI support enabled”,則證明nginx支持SNI。4.2 配置
4.2.1 Nginx作爲server
Nginx通過server命令配置多個virtual server,並通過server_name命令區分不同的Server Name,對應加載不同的SSL配置:server {
listen 443 ssl;
server_name servername1.net;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
ssl_protocols TLSv1.2;
ssl_ciphers AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5;
ssl_verify_depth 1;
ssl_certificate /root/server1.cer;
ssl_certificate_key /root/server1.key;
…
}
server {
listen 443 ssl;
server_name servername2.net;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
ssl_protocols TLSv1.2;
ssl_ciphers AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5;
ssl_verify_depth 1;
ssl_certificate /root/server2.cer;
ssl_certificate_key /root/server2.key;
…
}
如果client訪問servername1.net,server會返回server1.cer證書;如果訪問servername2.net,則返回server2.cer證書。這樣的配置允許client訪問相同的IP但使用不同的域名,server會根據域名的不同返回相應的證書。Client的應用程序應該將域名信息添加到ClientHello的server_name擴展中才能使得server支持SNI功能。
注:配置文件中ssl_certificate所指向的證書中CN必須與server_name所配置的域名完全一致。
4.2.2 Nginx作爲client
Nginx的proxy模式可以使nginx與後端SSL server建立SSL連接,這時nginx作爲ssl client可以使用SNI功能。配置舉例:upstream servername2.net {
server 192.168.135.128:447;
}
server {
listen 8081;
ssl_protocols TLSv1.2;
proxy_ssl_server_name on;
proxy_ssl_verify on;
#proxy_ssl_name servername1.net;
proxy_ssl_trusted_certificate /home/remy/ca-server.cer;
ssl_ciphers AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5;
location / {
proxy_pass https://servername2.net;
}
}
其中的關鍵配置是“proxy_ssl_server_name on”、“proxy_pass https://servername2.net”和“upstream servername2.net”相關配置。其中upstream的名字必須與server的域名一致(即4.2.1中的server_name命令所配置的域名)。也可以使用proxy_ssl_name命令設置server name,這個設置會替代proxy_pass中的host name被填入到ClientHello的server_name擴展中。4.3 代碼分析(基於nginx-1.12.1)
4.3.1 Nginx作爲server
Nginx在解析server_name命令時使用ngx_http_core_server_name()函數將所有的server_name保存到隊列中:4297 static char *
4298 ngx_http_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
4299 {
4300 ngx_http_core_srv_conf_t *cscf = conf;
4301
4302 u_char ch;
4303 ngx_str_t *value;
4304 ngx_uint_t i;
4305 ngx_http_server_name_t *sn;
…
4327 sn = ngx_array_push(&cscf->server_names);
…
4335 sn->server = cscf;
4336
4337 if (ngx_strcasecmp(value[i].data, (u_char *) "$hostname") == 0) {
4338 sn->name = cf->cycle->hostname;
4339
4340 } else {
4341 sn->name = value[i];
4342 }
…
4349 #if (NGX_PCRE)
4350 {
4351 u_char *p;
4352 ngx_regex_compile_t rc;
4353 u_char errstr[NGX_MAX_CONF_ERRSTR];
4354
4355 if (value[i].len == 1) {
4356 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
4357 "empty regex in server name \"%V\"", &value[i]);
4358 return NGX_CONF_ERROR;
4359 }
4360
4361 value[i].len--;
4362 value[i].data++;
4363
4364 ngx_memzero(&rc, sizeof(ngx_regex_compile_t));
4365
4366 rc.pattern = value[i];
4367 rc.err.len = NGX_MAX_CONF_ERRSTR;
4368 rc.err.data = errstr;
4369
4370 for (p = value[i].data; p < value[i].data + value[i].len; p++) {
4371 if (*p >= 'A' && *p <= 'Z') {
4372 rc.options = NGX_REGEX_CASELESS;
4373 break;
4374 }
4375 }
4376
4377 sn->regex = ngx_http_regex_compile(cf, &rc);
4378 if (sn->regex == NULL) {
4379 return NGX_CONF_ERROR;
4380 }
4381
4382 sn->name = value[i];
4383 cscf->captures = (rc.captures > 0);
4384 }
4385 #else
…
這樣一個server_name就會通過cscf的server_names成員與一個cscf關聯,而一個cscf會與一個CTX關聯(從而可以與一個server證書關聯)。詳見下文。在merge config時,nginx會調用SSL_CTX_set_tlsext_servername_callback()函數設置回調:
562 static char *
563 ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
564 {
…
671 if (ngx_ssl_create(&conf->ssl, conf->protocols, conf) != NGX_OK) {
672 return NGX_CONF_ERROR;
673 }
674
675 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
676
677 if (SSL_CTX_set_tlsext_servername_callback(conf->ssl.ctx,
678 ngx_http_ssl_servername)
679 == 0)
680 {
681 ngx_log_error(NGX_LOG_WARN, cf->log, 0,
682 "nginx was built with SNI support, however, now it is linked "
683 "dynamically to an OpenSSL library which has no tlsext support, "
684 "therefore SNI is not available");
685 }
686
687 #endif
…
706 if (ngx_ssl_certificates(cf, &conf->ssl, conf->certificates,
707 conf->certificate_keys, conf->passwords)
708 != NGX_OK)
709 {
710 return NGX_CONF_ERROR;
711 }
…
671:爲當前的ngx_http_ssl_srv_conf_t配置創建CTX結構並進行設置;706:將證書載入到CTX結構中。
在解析server指令下listen指令所配置的端口時,nginx會執行如下操作:
1790 static ngx_int_t
1791 ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,
1792 ngx_http_conf_addr_t *addr)
1793 {
1794 ngx_uint_t i;
1795 ngx_http_in_addr_t *addrs;
1796 struct sockaddr_in *sin;
1797 ngx_http_virtual_names_t *vn;
1798
1799 hport->addrs = ngx_pcalloc(cf->pool,
1800 hport->naddrs * sizeof(ngx_http_in_addr_t));
1801 if (hport->addrs == NULL) {
1802 return NGX_ERROR;
1803 }
1804
1805 addrs = hport->addrs;
1806
1807 for (i = 0; i < hport->naddrs; i++) {
…
1833 vn = ngx_palloc(cf->pool, sizeof(ngx_http_virtual_names_t));
1834 if (vn == NULL) {
1835 return NGX_ERROR;
1836 }
1837
1838 addrs[i].conf.virtual_names = vn;
1839
1840 vn->names.hash = addr[i].hash;
1841 vn->names.wc_head = addr[i].wc_head;
1842 vn->names.wc_tail = addr[i].wc_tail;
1843 #if (NGX_PCRE)
1844 vn->nregex = addr[i].nregex;
1845 vn->regex = addr[i].regex;
1846 #endif
1847 }
1848
1849 return NGX_OK;
1850 }
這樣addrs[i].conf.virtual_names就與addr[i].hash、addr[i].wc_head、addr[i].wc_tail關聯起來。對於4.2.1中的配置(兩個server block,端口一樣,server_name不同),i爲0。在解析http block的最後,nginx會調用ngx_http_optimize_servers()函數優化端口和server name列表:
1379 static ngx_int_t
1380 ngx_http_optimize_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
1381 ngx_array_t *ports)
1382 {
1383 ngx_uint_t p, a;
1384 ngx_http_conf_port_t *port;
1385 ngx_http_conf_addr_t *addr;
1386
1387 if (ports == NULL) {
1388 return NGX_OK;
1389 }
1390
1391 port = ports->elts;
1392 for (p = 0; p < ports->nelts; p++) {
1393
1394 ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,
1395 sizeof(ngx_http_conf_addr_t), ngx_http_cmp_conf_addrs);
1396
1397 /*
1398 * check whether all name-based servers have the same
1399 * configuration as a default server for given address:port
1400 */
1401
1402 addr = port[p].addrs.elts;
1403 for (a = 0; a < port[p].addrs.nelts; a++) {
1404
1405 if (addr[a].servers.nelts > 1
1406 #if (NGX_PCRE)
1407 || addr[a].default_server->captures
1408 #endif
1409 )
1410 {
1411 if (ngx_http_server_names(cf, cmcf, &addr[a]) != NGX_OK) {
1412 return NGX_ERROR;
1413 }
1414 }
1415 }
…
1411:ngx_http_server_names()函數優化serve name列表:1426 static ngx_int_t
1427 ngx_http_server_names(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
1428 ngx_http_conf_addr_t *addr)
1429 {
1430 ngx_int_t rc;
1431 ngx_uint_t n, s;
1432 ngx_hash_init_t hash;
1433 ngx_hash_keys_arrays_t ha;
1434 ngx_http_server_name_t *name;
1435 ngx_http_core_srv_conf_t **cscfp;
1436 #if (NGX_PCRE)
1437 ngx_uint_t regex, i;
1438
1439 regex = 0;
1440 #endif
…
1451 if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) {
1452 goto failed;
1453 }
1454
1455 cscfp = addr->servers.elts;
1456
1457 for (s = 0; s < addr->servers.nelts; s++) {
1458
1459 name = cscfp[s]->server_names.elts;
1460
1461 for (n = 0; n < cscfp[s]->server_names.nelts; n++) {
1462
1463 #if (NGX_PCRE)
1464 if (name[n].regex) {
1465 regex++;
1466 continue;
1467 }
1468 #endif
1469
1470 rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,
1471 NGX_HASH_WILDCARD_KEY);
…
1492 hash.key = ngx_hash_key_lc;
1493 hash.max_size = cmcf->server_names_hash_max_size;
1494 hash.bucket_size = cmcf->server_names_hash_bucket_size;
1495 hash.name = "server_names_hash";
1496 hash.pool = cf->pool;
1497
1498 if (ha.keys.nelts) {
1499 hash.hash = &addr->hash;
1500 hash.temp_pool = NULL;
1501
1502 if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {
1503 goto failed;
1504 }
1505 }
…
1545 #if (NGX_PCRE)
1546
1547 if (regex == 0) {
1548 return NGX_OK;
1549 }
1550
1551 addr->nregex = regex;
1552 addr->regex = ngx_palloc(cf->pool, regex * sizeof(ngx_http_server_name_t));
1553 if (addr->regex == NULL) {
1554 return NGX_ERROR;
1555 }
1556
1557 i = 0;
1558
1559 for (s = 0; s < addr->servers.nelts; s++) {
1560
1561 name = cscfp[s]->server_names.elts;
1562
1563 for (n = 0; n < cscfp[s]->server_names.nelts; n++) {
1564 if (name[n].regex) {
1565 addr->regex[i++] = name[n];
1566 }
1567 }
1568 }
1569
1570 #endif
1571
1572 return NGX_OK;
…
1451-1471:將所有的server name全部加入到ha hash隊列中;1492-1504:將ha hash隊列與addr->hash相關聯(addr與一個IP|port對一一對應),從而與前面提到的addrs[i].conf.virtual_names關聯起來;
1551-1568:將所有server name的正則表達式信息全部添加到addr->regex數組中。
至此,server name的配置文件解析工作全部完成,接下來需要在nginx處理ClientHello的SNI回調函數中利用上述關聯關係實現server name與SSL配置(證書等)的綁定。
SSL_CTX_set_tlsext_servername_callback()函數所設置的ngx_http_ssl_servername()函數會作爲回調完成Server Name匹配的相關核心工作:
825 int
826 ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
827 {
828 ngx_str_t host;
829 const char *servername;
830 ngx_connection_t *c;
831 ngx_http_connection_t *hc;
832 ngx_http_ssl_srv_conf_t *sscf;
833 ngx_http_core_loc_conf_t *clcf;
834 ngx_http_core_srv_conf_t *cscf;
835
836 servername = SSL_get_servername(ssl_conn, TLSEXT_NAMETYPE_host_name);
…
842 c = ngx_ssl_get_connection(ssl_conn);
…
851 host.len = ngx_strlen(servername);
852
853 if (host.len == 0) {
854 return SSL_TLSEXT_ERR_NOACK;
855 }
856
857 host.data = (u_char *) servername;
…
863 hc = c->data;
864
865 if (ngx_http_find_virtual_server(c, hc->addr_conf->virtual_names, &host,
866 NULL, &cscf)
867 != NGX_OK)
868 {
869 return SSL_TLSEXT_ERR_NOACK;
870 }
…
885 sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);
…
889 if (sscf->ssl.ctx) {
890 SSL_set_SSL_CTX(ssl_conn, sscf->ssl.ctx);
891
892 /*
893 * SSL_set_SSL_CTX() only changes certs as of 1.0.0d
894 * adjust other things we care about
895 */
896
897 SSL_set_verify(ssl_conn, SSL_CTX_get_verify_mode(sscf->ssl.ctx),
898 SSL_CTX_get_verify_callback(sscf->ssl.ctx));
899
900 SSL_set_verify_depth(ssl_conn, SSL_CTX_get_verify_depth(sscf->ssl.ctx));
901
902 #ifdef SSL_CTRL_CLEAR_OPTIONS
903 /* only in 0.9.8m+ */
904 SSL_clear_options(ssl_conn, SSL_get_options(ssl_conn) &
905 ~SSL_CTX_get_options(sscf->ssl.ctx));
906 #endif
907
908 SSL_set_options(ssl_conn, SSL_CTX_get_options(sscf->ssl.ctx));
909 }
…
836:從ClientHello的server_name擴展中獲取server name;851-857:將serve name記錄在host中;
865:通過與IP|port綁定的addr_conf的virtual_names成員和host關鍵字找到與server name對應的配置文件指針cscf;
885-909:找到與cscf相關聯的CTX結構體指針,將其與ssl結構關聯,後續在發送證書時就會使用server name對應的CTX所載入的證書。
我們不妨來看看ngx_http_find_virtual_server()函數是如何查找server name對應的配置的:
2101 static ngx_int_t
2102 ngx_http_find_virtual_server(ngx_connection_t *c,
2103 ngx_http_virtual_names_t *virtual_names, ngx_str_t *host,
2104 ngx_http_request_t *r, ngx_http_core_srv_conf_t **cscfp)
2105 {
2106 ngx_http_core_srv_conf_t *cscf;
2107
2108 if (virtual_names == NULL) {
2109 return NGX_DECLINED;
2110 }
2111
2112 cscf = ngx_hash_find_combined(&virtual_names->names,
2113 ngx_hash_key(host->data, host->len),
2114 host->data, host->len);
2115
2116 if (cscf) {
2117 *cscfp = cscf;
2118 return NGX_OK;
2119 }
2120
2121 #if (NGX_PCRE)
2122
2123 if (host->len && virtual_names->nregex) {
2124 ngx_int_t n;
2125 ngx_uint_t i;
2126 ngx_http_server_name_t *sn;
2127
2128 sn = virtual_names->regex;
2129
2130 #if (NGX_HTTP_SSL && defined SSL_CTRL_SET_TLSEXT_HOSTNAME)
2131
2132 if (r == NULL) {
2133 ngx_http_connection_t *hc;
2134
2135 for (i = 0; i < virtual_names->nregex; i++) {
2136
2137 n = ngx_regex_exec(sn[i].regex->regex, host, NULL, 0);
2138
2139 if (n == NGX_REGEX_NO_MATCHED) {
2140 continue;
2141 }
2142
2143 if (n >= 0) {
2144 hc = c->data;
2145 hc->ssl_servername_regex = sn[i].regex;
2146
2147 *cscfp = sn[i].server;
2148 return NGX_OK;
2149 }
2150
2151 ngx_log_error(NGX_LOG_ALERT, c->log, 0,
2152 ngx_regex_exec_n " failed: %i "
2153 "on \"%V\" using \"%V\"",
2154 n, host, &sn[i].regex->name);
2155
2156 return NGX_ERROR;
2157 }
2158
2159 return NGX_DECLINED;
2160 }
…
2112-2119:ngx_hash_find_combined()函數在virtual_names->names.hash(即addr->hash)中保存的全部serve name的列表中根據server name關鍵字來查詢cscf:210 void *
211 ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name,
212 size_t len)
213 {
214 void *value;
215
216 if (hash->hash.buckets) {
217 value = ngx_hash_find(&hash->hash, key, name, len);
218
219 if (value) {
220 return value;
221 }
222 }
…
2123-2149:如果上述流程沒有查詢到,則遍歷virtual_names->regex(即addr->regex)進行查找。將所有server name信息添加到addr->hash和addr->regex隊列都是在ngx_http_server_names()函數中完成的,而將virtual_names->names.hash與addr->hash關聯、將virtual_names->regex與addr->regex關聯是在ngx_http_add_addrs()函數中完成的。
Nginx中server name與cscf以及CTX的關聯、server name信息添加到addr hash表/隊列、virtual server查找等整個過程真心太複雜了!
4.3.2 Nginx作爲client
作爲SSL client支持SNI功能,nginx的實現比較簡單,就是調用SSL_set_tlsext_host_name()函數設置host_name擴展:1750 static ngx_int_t
1751 ngx_http_upstream_ssl_name(ngx_http_request_t *r, ngx_http_upstream_t *u,
1752 ngx_connection_t *c)
1753 {
1754 u_char *p, *last;
1755 ngx_str_t name;
1756
1757 if (u->conf->ssl_name) {
1758 if (ngx_http_complex_value(r, u->conf->ssl_name, &name) != NGX_OK) {
1759 return NGX_ERROR;
1760 }
1761
1762 } else {
1763 name = u->ssl_name;
1764 }
…
1825 if (SSL_set_tlsext_host_name(c->ssl->connection,
1826 (char *) name.data)
1827 == 0)
1828 {
1829 ngx_ssl_error(NGX_LOG_ERR, r->connection->log, 0,
1830 "SSL_set_tlsext_host_name(\"%s\") failed", name.data);
1831 return NGX_ERROR;
1832 }
…
1757-1764:如果使用proxy_ssl_name命令設置了server name,則優先使用;否則使用host name作爲server name;1825-1832:將server name填入ClientHello的server_name擴展中。
五、HAProxy與SNI
5.1 配置舉例
5.1.1 HAProxy作爲server
HAProxy有兩種方式支持SNI:1) 根據證書的CN名進行匹配;配置如下:
global
maxconn 100
defaults
mode http
timeout connect 5s
timeout client 5s
timeout server 5s
frontend myfrontend
# primary cert is /etc/cert/server.pem
# /etc/cert/certdir/ contains additional certificates for SNI clients
bind :446 ssl crt /etc/cert/server.pem crt /etc/cert/certdir/
default_backend mybackend
backend mybackend
# a http backend
server s3 192.168.135.1:80
使用這種方式不需要特殊配置,只要載入證書,HAProxy就會根據證書中的CN名去匹配ClientHello中的server_name擴展。2) 爲每個證書指定匹配的域名:
global
maxconn 100
defaults
mode http
timeout connect 5s
timeout client 5s
timeout server 5s
frontend myfrontend
bind :446 ssl crt-list /root/cert.list
default_backend mybackend
backend mybackend
# a http backend
server s3 192.168.135.1:80
其中cert.list的格式如下:/path/to/cert1.pem domain1.com
/path/to/cert2.pem domain2.com
這種方式在需要支持通配符或排除一些域名的情況下比較有用。5.1.2 HAProxy作爲client
HAProxy可以連接後端的SSL服務器,這時它作爲client可以使用sni命令設置SNI:global
maxconn 100
defaults
mode http
timeout connect 5s
timeout client 5s
timeout server 5s
frontend myfrontend
bind :808
default_backend mybackend
backend mybackend
# a https backend
server s4 192.168.135.128:446 ssl verify none sni str(domain1.com)
5.2 代碼分析
5.2.1 HAProxy作爲server
5.2.1.1 基於證書的CN名
HAProxy在解析crt命令時會調用bind_parse_crt()函數:5207 /* parse the "crt" bind keyword */
5208 static int bind_parse_crt(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
5209 {
…
5217 if ((*args[cur_arg + 1] != '/' ) && global.crt_base) {
5218 if ((strlen(global.crt_base) + 1 + strlen(args[cur_arg + 1]) + 1) > MAXPATHLEN) {
5219 memprintf(err, "'%s' : path too long", args[cur_arg]);
5220 return ERR_ALERT | ERR_FATAL;
5221 }
5222 snprintf(path, sizeof(path), "%s/%s", global.crt_base, args[cur_arg + 1]);
5223 if (ssl_sock_load_cert(path, conf, px, err) > 0)
5224 return ERR_ALERT | ERR_FATAL;
5225
5226 return 0;
5227 }
5228
5229 if (ssl_sock_load_cert(args[cur_arg + 1], conf, px, err) > 0)
5230 return ERR_ALERT | ERR_FATAL;
5231
5232 return 0;
5233 }
其中的關鍵函數是ssl_sock_load_cert()函數:2452 int ssl_sock_load_cert(char *path, struct bind_conf *bind_conf, struct proxy *curproxy, char **err)
2453 {
…
2466 if (stat(path, &buf) == 0) {
2467 dir = opendir(path);
2468 if (!dir)
2469 return ssl_sock_load_cert_file(path, bind_conf, curproxy, NULL, 0, err);
…
2475 n = scandir(path, &de_list, 0, alphasort);
2476 if (n < 0) {
2477 memprintf(err, "%sunable to scan directory '%s' : %s.\n",
2478 err && *err ? *err : "", path, strerror(errno));
2479 cfgerr++;
2480 }
2481 else {
2482 for (i = 0; i < n; i++) {
2483 struct dirent *de = de_list[i];
2484
2485 end = strrchr(de->d_name, '.');
2486 if (end && (!strcmp(end, ".issuer") || !strcmp(end, ".ocsp") || !strcmp(end, ".sctl")))
2487 goto ignore_entry;
2488
2489 snprintf(fp, sizeof(fp), "%s/%s", path, de->d_name);
…
2499 #if OPENSSL_VERSION_NUMBER >= 0x1000200fL
2500 is_bundle = 0;
2501 /* Check if current entry in directory is part of a multi-cert bundle */
2502
2503 if (end) {
2504 for (j = 0; j < SSL_SOCK_NUM_KEYTYPES; j++) {
2505 if (!strcmp(end + 1, SSL_SOCK_KEYTYPE_NAMES[j])) {
2506 is_bundle = 1;
2507 break;
2508 }
2509 }
2510
2511 if (is_bundle) {
2512 char dp[MAXPATHLEN+1] = {0}; /* this will be the filename w/o the keytype */
2513 int dp_len;
2514
2515 dp_len = end - de->d_name;
2516 snprintf(dp, dp_len + 1, "%s", de->d_name);
2517
2518 /* increment i and free de until we get to a non-bundle cert
2519 * Note here that we look at de_list[i + 1] before freeing de
2520 * this is important since ignore_entry will free de
2521 */
2522 while (i + 1 < n && !strncmp(de_list[i + 1]->d_name, dp, dp_len)) {
2523 free(de);
2524 i++;
2525 de = de_list[i];
2526 }
2527
2528 snprintf(fp, sizeof(fp), "%s/%s", path, dp);
2529 ssl_sock_load_multi_cert(fp, bind_conf, curproxy, NULL, 0, err);
2530
2531 /* Successfully processed the bundle */
2532 goto ignore_entry;
2533 }
2534 }
2535
2536 #endif
2537 cfgerr += ssl_sock_load_cert_file(fp, bind_conf, curproxy, NULL, 0, err);
…
2466-2469:如果路徑可訪問,且指定的是文件而非目錄,則調用ssl_sock_load_cert_file()函數加載證書;2500-2534:如果文件的後綴名爲.dsa,.ecdsa,.rsa,則調用ssl_sock_load_multi_cert()函數將這些文件批量載入;
2537: 如果文件無上述後綴,則調用ssl_sock_load_cert_file()逐個加載目錄中所有的證書文件。
2547:如果路徑不能正常訪問,則調用ssl_sock_load_multi_cert ()函數將以這個路徑名爲名、以.dsa,.ecdsa,.rsa爲後綴的所有文件批量載入。
2364 static int ssl_sock_load_cert_file(const char *path, struct bind_conf *bind_conf, struct proxy *curproxy, char **sni_filter, int fcount, char **err)
2365 {
2366 int ret;
2367 SSL_CTX *ctx;
2368
2369 ctx = SSL_CTX_new(SSLv23_server_method());
…
2376 if (SSL_CTX_use_PrivateKey_file(ctx, path, SSL_FILETYPE_PEM) <= 0) {
2377 memprintf(err, "%sunable to load SSL private key from PEM file '%s'.\n",
2378 err && *err ? *err : "", path);
2379 SSL_CTX_free(ctx);
2380 return 1;
2381 }
2382
2383 ret = ssl_sock_load_cert_chain_file(ctx, path, bind_conf, sni_filter, fcount);
…
此函數爲每個證書文件創建一個CTX,加載私鑰文件,再調用ssl_sock_load_cert_chain_file()函數加載證書:2262 static int ssl_sock_load_cert_chain_file(SSL_CTX *ctx, const char *file, struct bind_conf *s, char **sni_filter, int fcount)
2263 {
…
2289 x = PEM_read_bio_X509_AUX(in, NULL, passwd_cb, passwd_cb_userdata);
2290 if (x == NULL)
2291 goto end;
2292
2293 if (fcount) {
2294 while (fcount--)
2295 order = ssl_sock_add_cert_sni(ctx, s, sni_filter[fcount], order);
2296 }
2297 else {
2298 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
2299 names = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
2300 if (names) {
2301 for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
2302 GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i);
2303 if (name->type == GEN_DNS) {
2304 if (ASN1_STRING_to_UTF8((unsigned char **)&str, name->d.dNSName) >= 0) {
2305 order = ssl_sock_add_cert_sni(ctx, s, str, order);
2306 OPENSSL_free(str);
2307 }
2308 }
2309 }
2310 sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
2311 }
2312 #endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
2313 xname = X509_get_subject_name(x);
2314 i = -1;
2315 while ((i = X509_NAME_get_index_by_NID(xname, NID_commonName, i)) != -1) {
2316 X509_NAME_ENTRY *entry = X509_NAME_get_entry(xname, i);
2317 ASN1_STRING *value;
2318
2319 value = X509_NAME_ENTRY_get_data(entry);
2320 if (ASN1_STRING_to_UTF8((unsigned char **)&str, value) >= 0) {
2321 order = ssl_sock_add_cert_sni(ctx, s, str, order);
2322 OPENSSL_free(str);
2323 }
2324 }
2325 }
2326
2327 ret = 0; /* the caller must not free the SSL_CTX argument anymore */
2328 if (!SSL_CTX_use_certificate(ctx, x))
2329 goto end;
…
2289:讀取證書文件;2293-2295:根據證書列表加載證書;對於crt命令這個邏輯不會執行;
2299-2310:根據證書中的Subject Alternative Name (SAN)設置SNI;SAN允許一個證書關聯多個域名;
2313-2325:根據證書中的CN設置SNI;設置SNI的功能由ssl_sock_add_cert_sni()函數完成:
1746 static int ssl_sock_add_cert_sni(SSL_CTX *ctx, struct bind_conf *s, char *name, int order)
1747 {
1748 struct sni_ctx *sc;
1749 int wild = 0, neg = 0;
1750 struct ebmb_node *node;
1751
1752 if (*name == '!') {
1753 neg = 1;
1754 name++;
1755 }
1756 if (*name == '*') {
1757 wild = 1;
1758 name++;
1759 }
1760 /* !* filter is a nop */
1761 if (neg && wild)
1762 return order;
1763 if (*name) {
1764 int j, len;
1765 len = strlen(name);
1766 for (j = 0; j < len && j < trash.size; j++)
1767 trash.str[j] = tolower(name[j]);
1768 if (j >= trash.size)
1769 return order;
1770 trash.str[j] = 0;
1771
1772 /* Check for duplicates. */
1773 if (wild)
1774 node = ebst_lookup(&s->sni_w_ctx, trash.str);
1775 else
1776 node = ebst_lookup(&s->sni_ctx, trash.str);
1777 for (; node; node = ebmb_next_dup(node)) {
1778 sc = ebmb_entry(node, struct sni_ctx, name);
1779 if (sc->ctx == ctx && sc->neg == neg)
1780 return order;
1781 }
1782
1783 sc = malloc(sizeof(struct sni_ctx) + len + 1);
1784 if (!sc)
1785 return order;
1786 memcpy(sc->name.key, trash.str, len + 1);
1787 sc->ctx = ctx;
1788 sc->order = order++;
1789 sc->neg = neg;
1790 if (wild)
1791 ebst_insert(&s->sni_w_ctx, &sc->name);
1792 else
1793 ebst_insert(&s->sni_ctx, &sc->name);
1794 }
1795 return order;
1796 }
這個函數的功能是將與域名關聯的sni_ctx結構加入到一棵以s->sni_ctx(域名不包含通配符)或s->sni_w_ctx(域名包含通配符)的Elastic Binary樹中。由於s是全局的,故樹也是全局的。在後續流程中檢查配置的合法性時,HAProxy會調用ssl_sock_prepare_all_ctx()函數初始化證書相關的配置:
3192 /* Walks down the two trees in bind_conf and prepares all certs. The pointer may
3193 * be NULL, in which case nothing is done. Returns the number of errors
3194 * encountered.
3195 */
3196 int ssl_sock_prepare_all_ctx(struct bind_conf *bind_conf, struct proxy *px)
3197 {
3198 struct ebmb_node *node;
3199 struct sni_ctx *sni;
3200 int err = 0;
3201
3202 if (!bind_conf || !bind_conf->is_ssl)
3203 return 0;
3204
3205 /* Automatic memory computations need to know we use SSL there */
3206 global.ssl_used_frontend = 1;
3207
3208 if (bind_conf->default_ctx)
3209 err += ssl_sock_prepare_ctx(bind_conf, bind_conf->default_ctx, px);
3210
3211 node = ebmb_first(&bind_conf->sni_ctx);
3212 while (node) {
3213 sni = ebmb_entry(node, struct sni_ctx, name);
3214 if (!sni->order && sni->ctx != bind_conf->default_ctx)
3215 /* only initialize the CTX on its first occurrence and
3216 if it is not the default_ctx */
3217 err += ssl_sock_prepare_ctx(bind_conf, sni->ctx, px);
3218 node = ebmb_next(node);
3219 }
3220
3221 node = ebmb_first(&bind_conf->sni_w_ctx);
3222 while (node) {
3223 sni = ebmb_entry(node, struct sni_ctx, name);
3224 if (!sni->order && sni->ctx != bind_conf->default_ctx)
3225 /* only initialize the CTX on its first occurrence and
3226 if it is not the default_ctx */
3227 err += ssl_sock_prepare_ctx(bind_conf, sni->ctx, px);
3228 node = ebmb_next(node);
3229 }
3230 return err;
3231 }
這個函數會遍歷conf->sni_ctx和bind_conf->sni_w_ctx這兩棵樹,並調用ssl_sock_prepare_ctx()初始化其中的所有節點:2682 int ssl_sock_prepare_ctx(struct bind_conf *bind_conf, SSL_CTX *ctx, struct proxy *curproxy)
2683 {
…
2882 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
2883 SSL_CTX_set_tlsext_servername_callback(ctx, ssl_sock_switchctx_cbk);
…
SSL_CTX_set_tlsext_servername_callback()函數所設置的回調函數ssl_sock_switchctx_cbk()用於處理ClientHello中的server_name擴展:1405 static int ssl_sock_switchctx_cbk(SSL *ssl, int *al, struct bind_conf *s)
1406 {
1407 const char *servername;
1408 const char *wildp = NULL;
1409 struct ebmb_node *node, *n;
1410 int i;
1411 (void)al; /* shut gcc stupid warning */
1412
1413 servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
…
1446 /* lookup in full qualified names */
1447 node = ebst_lookup(&s->sni_ctx, trash.str);
1448
1449 /* lookup a not neg filter */
1450 for (n = node; n; n = ebmb_next_dup(n)) {
1451 if (!container_of(n, struct sni_ctx, name)->neg) {
1452 node = n;
1453 break;
1454 }
1455 }
1456 if (!node && wildp) {
1457 /* lookup in wildcards names */
1458 node = ebst_lookup(&s->sni_w_ctx, wildp);
1459 }
…
1472 /* switch ctx */
1473 SSL_set_SSL_CTX(ssl, container_of(node, struct sni_ctx, name)->ctx);
1474 return SSL_TLSEXT_ERR_OK;
1475 }
1413:從ClientHello中獲取server name;1447-1458:先從s->sni_ctx查找加載證書時保存的節點,如果查不到則在s->sni_w_ctx中查找;
1473:將當前ssl結構與加載了和server name對應的證書的CTX結構相關聯,以實現後續功能(發送對應的證書等)。
5.2.1.2 基於配置文件指定的域名
HAProxy解析crt_list命令的函數是ssl_sock_load_cert_list_file():2568 int ssl_sock_load_cert_list_file(char *file, struct bind_conf *bind_conf, struct proxy *curproxy, char **err)
2569 {
2570 char thisline[LINESIZE*CRTLIST_FACTOR];
2571 FILE *f;
2572 struct stat buf;
2573 int linenum = 0;
2574 int cfgerr = 0;
2575
2576 if ((f = fopen(file, "r")) == NULL) {
2577 memprintf(err, "cannot open file '%s' : %s", file, strerror(errno));
2578 return 1;
2579 }
2580
2581 while (fgets(thisline, sizeof(thisline), f) != NULL) {
2582 int arg;
2583 int newarg;
2584 char *end;
2585 char *args[MAX_LINE_ARGS*CRTLIST_FACTOR + 1];
2586 char *line = thisline;
…
2601 newarg = 1;
2602 while (*line) {
2603 if (*line == '#' || *line == '\n' || *line == '\r') {
2604 /* end of string, end of loop */
2605 *line = 0;
2606 break;
2607 }
2608 else if (isspace(*line)) {
2609 newarg = 1;
2610 *line = 0;
2611 }
2612 else if (newarg) {
2613 if (arg == MAX_LINE_ARGS*CRTLIST_FACTOR) {
2614 memprintf(err, "too many args on line %d in file '%s'.",
2615 linenum, file);
2616 cfgerr = 1;
2617 break;
2618 }
2619 newarg = 0;
2620 args[arg++] = line;
2621 }
2622 line++;
2623 }
…
2631 if (stat(args[0], &buf) == 0) {
2632 cfgerr = ssl_sock_load_cert_file(args[0], bind_conf, curproxy, &args[1], arg-1, err);
2633 } else {
2634 cfgerr = ssl_sock_load_multi_cert(args[0], bind_conf, curproxy, &args[1], arg-1, err);
2635 }
…
2581-2623:逐行解析list文件,將每行的參數放置在args數組中;2631-2632:如果路徑名可以訪問,則調用ssl_sock_load_cert_file()函數解析證書文件,並傳入server name及其數量。這樣當ssl_sock_load_cert_file()函數調到ssl_sock_load_cert_chain_file()函數時就會執行如下語句:
2262 static int ssl_sock_load_cert_chain_file(SSL_CTX *ctx, const char *file, struct bind_conf *s, char **sni_filter, int fcount)
2263 {
…
2293 if (fcount) {
2294 while (fcount--)
2295 order = ssl_sock_add_cert_sni(ctx, s, sni_filter[fcount], order);
2296 }
…
2293-2295:將所有的server name(域名)與證書(及其CTX)的關聯關係加入到全局Elastic Binary樹中,這樣就允許一個證書與多個域名相關聯。2634:如果路徑名不能訪問,則調用ssl_sock_load_multi_cert ()函數將以這個路徑名爲名、以.dsa,.ecdsa,.rsa爲後綴的所有文件批量載入。
2032 /* Given a path that does not exist, try to check for path.rsa, path.dsa and path.ecdsa files.
2033 * If any are found, group these files into a set of SSL_CTX*
2034 * based on shared and unique CN and SAN entries. Add these SSL_CTX* to the SNI tree.
2035 *
2036 * This will allow the user to explictly group multiple cert/keys for a single purpose
2037 *
2038 * Returns
2039 * 0 on success
2040 * 1 on failure
2041 */
2042 static int ssl_sock_load_multi_cert(const char *path, struct bind_conf *bind_conf, struct proxy *curproxy, char **sni_filter, int fcount, char **err)
2043 {
2044 char fp[MAXPATHLEN+1] = {0};
2045 int n = 0;
2046 int i = 0;
2047 struct cert_key_and_chain certs_and_keys[SSL_SOCK_NUM_KEYTYPES] = { {0} };
2048 struct eb_root sni_keytypes_map = { {0} };
2049 struct ebmb_node *node;
2050 struct ebmb_node *next;
2051 /* Array of SSL_CTX pointers corresponding to each possible combo
2052 * of keytypes
2053 */
2054 struct key_combo_ctx key_combos[SSL_SOCK_POSSIBLE_KT_COMBOS] = { {0} };
2055 int rv = 0;
2056 X509_NAME *xname = NULL;
2057 char *str = NULL;
2058 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
2059 STACK_OF(GENERAL_NAME) *names = NULL;
2060 #endif
2061
2062 /* Load all possible certs and keys */
2063 for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
2064 struct stat buf;
2065
2066 snprintf(fp, sizeof(fp), "%s.%s", path, SSL_SOCK_KEYTYPE_NAMES[n]);
2067 if (stat(fp, &buf) == 0) {
2068 if (ssl_sock_load_crt_file_into_ckch(fp, &certs_and_keys[n], err) == 1) {
2069 rv = 1;
2070 goto end;
2071 }
2072 }
2073 }
…
2081 for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
2082
2083 if (!ssl_sock_is_ckch_valid(&certs_and_keys[n]))
2084 continue;
2085
2086 if (fcount) {
2087 for (i = 0; i < fcount; i++)
2088 ssl_sock_populate_sni_keytypes_hplr(sni_filter[i], &sni_keytypes_map, n);
2089 } else {
2090 /* A lot of the following code is OpenSSL boilerplate for processing CN's and SAN's,
2091 * so the line that contains logic is marked via comments
2092 */
2093 xname = X509_get_subject_name(certs_and_keys[n].cert);
2094 i = -1;
2095 while ((i = X509_NAME_get_index_by_NID(xname, NID_commonName, i)) != -1) {
2096 X509_NAME_ENTRY *entry = X509_NAME_get_entry(xname, i);
2097 ASN1_STRING *value;
2098 value = X509_NAME_ENTRY_get_data(entry);
2099 if (ASN1_STRING_to_UTF8((unsigned char **)&str, value) >= 0) {
2100 /* Important line is here */
2101 ssl_sock_populate_sni_keytypes_hplr(str, &sni_keytypes_map, n);
2102
2103 OPENSSL_free(str);
2104 str = NULL;
2105 }
2106 }
2107
2108 /* Do the above logic for each SAN */
2109 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
2110 names = X509_get_ext_d2i(certs_and_keys[n].cert, NID_subject_alt_name, NULL, NULL);
2111 if (names) {
2112 for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
2113 GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i);
2114
2115 if (name->type == GEN_DNS) {
2116 if (ASN1_STRING_to_UTF8((unsigned char **)&str, name->d.dNSName) >= 0) {
2117 /* Important line is here */
2118 ssl_sock_populate_sni_keytypes_hplr(str, &sni_keytypes_map, n);
2119
2120 OPENSSL_free(str);
2121 str = NULL;
2122 }
2123 }
2124 }
2125 }
2126 }
2127 #endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
2128 }
…
2152 node = ebmb_first(&sni_keytypes_map);
2153 while (node) {
2154 SSL_CTX *cur_ctx;
2155 char cur_file[MAXPATHLEN+1];
2156
2157 str = (char *)container_of(node, struct sni_keytype, name)->name.key;
2158 i = container_of(node, struct sni_keytype, name)->keytypes;
2159 cur_ctx = key_combos[i-1].ctx;
2160
2161 if (cur_ctx == NULL) {
2162 /* need to create SSL_CTX */
2163 cur_ctx = SSL_CTX_new(SSLv23_server_method());
2164 if (cur_ctx == NULL) {
2165 memprintf(err, "%sunable to allocate SSL context.\n",
2166 err && *err ? *err : "");
2167 rv = 1;
2168 goto end;
2169 }
…
2170
2171 /* Load all required certs/keys/chains/OCSPs info into SSL_CTX */
2172 for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
2173 if (i & (1<<n)) {
2174 /* Key combo contains ckch[n] */
2175 snprintf(cur_file, MAXPATHLEN+1, "%s.%s", path, SSL_SOCK_KEYTYPE_NAMES[n]);
2176 if (ssl_sock_put_ckch_into_ctx(cur_file, &certs_and_keys[n], cur_ctx, err) != 0) {
2177 SSL_CTX_free(cur_ctx);
2178 rv = 1;
2179 goto end;
2180 }
2181
2182 #if (defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP)
2183 /* Load OCSP Info into context */
2184 if (ssl_sock_load_ocsp(cur_ctx, cur_file) < 0) {
2185 if (err)
2186 memprintf(err, "%s '%s.ocsp' is present and activates OCSP but it is impossible to compute the OCSP certificate ID (maybe the issuer could not be found)'.\n",
2187 *err ? *err : "", cur_file);
2188 SSL_CTX_free(cur_ctx);
2189 rv = 1;
2190 goto end;
2191 }
2192 #endif
2193 }
2194 }
…
2211 /* Update key_combos */
2212 key_combos[i-1].ctx = cur_ctx;
2213 }
2214
2215 /* Update SNI Tree */
2216 key_combos[i-1].order = ssl_sock_add_cert_sni(cur_ctx, bind_conf, str, key_combos[i-1].order);
2217 node = ebmb_next(node);
2218 }
…
2063-2073:將所有證書|私鑰載入到certs_and_keys數組中;2081-2127:遍歷所有類型的證書(.rsa,.dsa,.ecdsa),將其所有的域名加入到入到sni_keytypes_map結構中。其中2087-2088行是將證書列表文件指定的域名加入到sni_keytypes_map結構中,2093-2126是將證書中的CN名和SAN名入到sni_keytypes_map結構中。相同的名稱會擁有相同的節點,每個節點都會有自己的keytypes數值(即證書類型組合)。多個域名可能會擁有相同的keytpes值。
2152-2218:遍歷sni_keytypes_map的所有節點,根據節點中的keytypes數值(即證書類型組合)查找或創建CTX,將擁有相同類型組合的證書和私鑰載入到CTX中(2172-2194),最後調用ssl_sock_add_cert_sni()將域名與CTX綁定關係加入到全局Elastic Binary樹中。
ssl_sock_load_multi_cert()函數可以將擁有相同CN名或SAN名的證書加載到同一個CTX中,這樣就能支持client端在訪問同一個域名時通過不同的cipher suite來選擇不同類型的證書(RSA,ECDSA)。但當前版本的HAProxy(haproxy-1.7.9)尚不支持多類型證書的證書鏈模式。
5.2.2 HAProxy作爲client
HAProxy通過backend指令中的sni指令來實現作爲client時對SNI的設置,這個指令的解析函數爲:5831 /* parse the "sni" server keyword */
5832 static int srv_parse_sni(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err)
5833 {
5834 #ifndef SSL_CTRL_SET_TLSEXT_HOSTNAME
5835 memprintf(err, "'%s' : the current SSL library doesn't support the SNI TLS extension", args[*cur_arg]);
5836 return ERR_ALERT | ERR_FATAL;
5837 #else
5838 int idx;
5839 struct sample_expr *expr;
5840
5841 if (!*args[*cur_arg + 1]) {
5842 memprintf(err, "'%s' : missing sni expression", args[*cur_arg]);
5843 return ERR_ALERT | ERR_FATAL;
5844 }
5845
5846 idx = (*cur_arg) + 1;
5847 px->conf.args.ctx = ARGC_SRV;
5848
5849 expr = sample_parse_expr((char **)args, &idx, px->conf.file, px->conf.line, err, &px->conf.args);
5850 if (!expr) {
5851 memprintf(err, "error detected while parsing sni expression : %s", *err);
5852 return ERR_ALERT | ERR_FATAL;
5853 }
5854
5855 if (!(expr->fetch->val & SMP_VAL_BE_SRV_CON)) {
5856 memprintf(err, "error detected while parsing sni expression : "
5857 " fetch method '%s' extracts information from '%s', none of which is available here.\n",
5858 args[idx-1], sample_src_names(expr->fetch->use));
5859 return ERR_ALERT | ERR_FATAL;
5860 }
5861
5862 px->http_needed |= !!(expr->fetch->use & SMP_USE_HTTP_ANY);
5863 newsrv->ssl_ctx.sni = expr;
5864 return 0;
5865 #endif
5866 }
解析的結果轉換爲表達式保存在struct server結果的ssl_ctx.sni成員中。在連接sever時使用:1029 int connect_server(struct stream *s)
1030 {
1031 struct connection *cli_conn;
1032 struct connection *srv_conn;
1033 struct connection *old_conn;
1034 struct server *srv;
1035 int reuse = 0;
1036 int err;
…
1207 #ifdef USE_OPENSSL
1208 if (srv->ssl_ctx.sni) {
1209 struct sample *smp;
1210 int rewind;
1211
1212 /* Tricky case : we have already scheduled the pending
1213 * HTTP request or TCP data for leaving. So in HTTP we
1214 * rewind exactly the headers, otherwise we rewind the
1215 * output data.
1216 */
1217 rewind = s->txn ? http_hdr_rewind(&s->txn->req) : s->req.buf->o;
1218 b_rew(s->req.buf, rewind);
1219
1220 smp = sample_fetch_as_type(s->be, s->sess, s, SMP_OPT_DIR_REQ | SMP_OPT_FINAL, srv->ssl_ctx.sni, SMP_T_STR);
1221
1222 /* restore the pointers */
1223 b_adv(s->req.buf, rewind);
1224
1225 if (smp_make_safe(smp)) {
1226 ssl_sock_set_servername(srv_conn, smp->data.u.str.str);
1227 srv_conn->flags |= CO_FL_PRIVATE;
1228 }
1229 }
1230 #endif /* USE_OPENSSL */
…
1208-1229:將srv->ssl_ctx.sni中的表達式轉換爲字符串保存在smp->data.u.str.str中,然後調用ssl_sock_set_servername()函數設置server name:4158 void ssl_sock_set_servername(struct connection *conn, const char *hostname)
4159 {
4160 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
4161 char *prev_name;
4162
4163 if (!ssl_sock_is_ssl(conn))
4164 return;
4165
4166 /* if the SNI changes, we must destroy the reusable context so that a
4167 * new connection will present a new SNI. As an optimization we could
4168 * later imagine having a small cache of ssl_ctx to hold a few SNI per
4169 * server.
4170 */
4171 prev_name = (char *)SSL_get_servername(conn->xprt_ctx, TLSEXT_NAMETYPE_host_name);
4172 if ((!prev_name && hostname) ||
4173 (prev_name && (!hostname || strcmp(hostname, prev_name) != 0)))
4174 SSL_set_session(conn->xprt_ctx, NULL);
4175
4176 SSL_set_tlsext_host_name(conn->xprt_ctx, hostname);
4177 #endif
4178 }
4176:將hostname設置到ClientHello的server_name擴展中。