HTTPS SNI Host and Envoy

TLS handshake

TLS 握手之前需要先 TCP 三次握手,之后就是正式的 Client hello, Server hello, Cert key exchange 等等正常的 TLS 握手动作了。这期间会决定使用的加密版本,协商用来对称加密的密钥等。Cloudflare 这篇文章似乎讲的很清楚。

HTTP2

http2 并不是必须使用 https 部署。但是因为主流浏览器都只支持基于 https 的 http2,所以如果服务是面向浏览器的,那还是应该使用 https。这篇文章讲的很好。

我常遇到的一个问题是,会有用户把 plain 的 http2 的请求发给只支持 https 的服务器。这个时候的表现是请求等一段时间会超时。服务器端那边能看到 "PRI * HTTP/2.0" 400 这样的错误信息。这是因为客户端期待通过 Magic 桢完成 http2 协商,但是服务器端认为是错误的请求,所以返回 400。

对于这样的错误,一般是设置通过 https 方式访问就好。例如 grpcurl 不要用那个 -plaintext 参数,envoy 使用 tls transport 等。

Host

一个 ip 和端口下面提供多个网站的服务,这个很常见了。那么用户连接过来的时候是如何区分的呢?http 协议里面,规定了可以使用 Host 这个 header 来制定需要访问的主机是哪个。例如下面这个请求。如果不指定 Host,一般会给返回默认主机的内容。

> GET / HTTP/2
> Host: www.qunar.com
> User-Agent: curl/7.64.1
> Accept: */*

SNI

如果一个 ip 和端口为多个域名提供 https 服务,那用户发起 tls 握手的时候,也得知道给用户返回哪个域名的证书才行(当然你也可以用通配符证书)。因为这个时候还没有握手成功,http 协议规定的那些都还没发生呢,所以 Host header 是不能用了。SNI 作为 tls 的一个扩展就是解决这个问题的。

SNI 数据做为握手数据是明文发送的,当然现在也有 ESNI 但是还没有普及。服务器根据 SNI 数据返回对应的证书。

Host + SNI?

上面提到这两个有各自的用途,所以你会发现,他们对应的域名其实可以不一样。例如你知道一个服务器上面同时为几个域名提供服务,那大概你可以 TLS 握手使用一个域名,实际 http 协议使用另一个域名。例如下面这个。

curl -I -v --http2 -H 'Host: www.qunar.com' https://qunar.com
*   Trying 117.122.224.176...
* TCP_NODELAY set
* Connected to qunar.com (117.122.224.176) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=CN; ST=Beijing; L=Beijing; O=\U5317\U4EAC\U8DA3\U62FF\U4FE1\U606F\U6280\U672F\U6709\U9650\U516C\U53F8; CN=qunar.com
*  start date: Nov 15 10:36:04 2019 GMT
*  expire date: Feb  8 23:59:59 2022 GMT
*  subjectAltName: host "qunar.com" matched cert's "qunar.com"
*  issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7f8cfd00f800)
> HEAD / HTTP/2
> Host: www.qunar.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200
HTTP/2 200
< date: Sun, 21 Feb 2021 06:30:45 GMT
date: Sun, 21 Feb 2021 06:30:45 GMT
< content-type: text/html; charset=utf-8
content-type: text/html; charset=utf-8
< content-length: 206076
content-length: 206076
< set-cookie: QN1=00001480306c2f4420a844ed; Expires=Thu, 31-Dec-37 23:55:55 GMT; Max-Age=31536000; Domain=qunar.com; Path=/
set-cookie: QN1=00001480306c2f4420a844ed; Expires=Thu, 31-Dec-37 23:55:55 GMT; Max-Age=31536000; Domain=qunar.com; Path=/
< x-powered-by: QXF/1.1.1
x-powered-by: QXF/1.1.1
< cache-control: no-cache
cache-control: no-cache
< expires: 0
expires: 0
< etag: W/"9sjEldYHMh6Ne03Bxy1MFA=="
etag: W/"9sjEldYHMh6Ne03Bxy1MFA=="
< vary: Accept-Encoding
vary: Accept-Encoding
< server: QWS/1.0
server: QWS/1.0
< req-id: 00001480306c2f4420a844ed
req-id: 00001480306c2f4420a844ed
< p3p: policyref="/w3c/p3p.xml", CP="CUR ADM OUR NOR STA NID"
p3p: policyref="/w3c/p3p.xml", CP="CUR ADM OUR NOR STA NID"
< set-cookie: QN300=organic; Domain=qunar.com; Max-Age=630720000; Path=/
set-cookie: QN300=organic; Domain=qunar.com; Max-Age=630720000; Path=/
< set-cookie: QN99=7459; Domain=qunar.com; expires=Thu, 31-Dec-2081 23:55:55 GMT;Max-Age=630720000; Path=/
set-cookie: QN99=7459; Domain=qunar.com; expires=Thu, 31-Dec-2081 23:55:55 GMT;Max-Age=630720000; Path=/
< cache-status: BYPASS
cache-status: BYPASS

<
* Connection #0 to host qunar.com left intact
* Closing connection 0

可以看到 * subjectAltName: host "qunar.com" matched cert's "qunar.com" 这里对应的域名是 qunar.com。后面 Host: www.qunar.com 是另外一个。可以比较下使用和不使用 host 的返回结果。

Envoy

Enovy 里面如果想要配置一个 cluster 是 http2 的话,可以参考下面的例子。

 transport_socket:
   name: envoy.transport_sockets.tls
   typed_config:
     "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
     sni: abc.com
     common_tls_context:
       alpn_protocols: [ "h2,http/1.1" ]
       validation_context:
         match_subject_alt_names:
         - exact: "abc.com"
         trusted_ca:
           filename: /etc/ssl/cert.pem

这个配置里面, envoy.transport_sockets.tls 使用 tls 连接目标服务器,要不然会遇到上面提到的 PRI * 400 那个错误。 sni: abc.com 用来告诉服务器和哪个域名做 TLS 握手。 validation_context 部分指定如何做证书校验,如果没有这部分的话,envoy 不会做证书校验,就是说即使对方返回的证书并不是那个 SNI 指定的也不管。