TLS 1.3 当前(2018.10)支持与部署之现状

今年8月10日,历经三年有余,TLS 1.3 最终版本终于得以发布——RFC 8446. 关于 RFC 的详细介绍可以进一步阅读 A Detailed Look at RFC 8446 (a.k.a. TLS 1.3). TLS 1.3 因为其在握手延迟以及安全性上的改进 (可以参考拙文《TLS1.3/QUIC 是怎样做到 0-RTT 的》),毫不夸张的说,这是一件将深刻而长远影响互联网发展的技术里程碑。那么将 TLS 1.3 尽快平滑应用到线上环境无疑是一件势在必行的事情了。

在我日常的工作中,对于 TLS 1.3 的支持和部署主要关注两个层面: 编程语言(Go, Java)、 API gateway (Nginx) 和浏览器. 下面分别介绍一下这三个层面 TLS 1.3 的支持部署现状。(因为 RFC 8446 已经发布,因此本文中说的支持如无特殊说明,都是指对最终版本 RFC 8446 的支持。)

编程语言对 TLS 1.3 的支持

Go 方面,官方在 TLS 1.3 draft 阶段一直没有跟进。因此,有一个关于对 TLS 1.3 支持的 issue 从 2015 年 open 至今都没有关闭:crypto/tls: add support for TLS 1.3. 其实 Go 发布版本和改进标准库的效率还是挺高的,对于 TLS 1.3 上的“不作为”更多是因为 Go 在兼容性上的承诺导致其并适合在最终版发布前实现互不兼容的 draft 方案。

而 Go 1.11 的发布时间(2018.08.24)与 RFC 8446 的发布时间比较接近,没有足够时间实现并发布该特性。从 golang-dev 小组讨论 Re: crypto/tls and TLS 1.3 看,由于 1.11 没有实现 TLS 1.3 ,那么 1.12 实现 TLS 1.3 基本是板上钉钉的事了:

The key scheduling fact is that the Go 1.11 feature freeze is just a week away, so we decided that it would be too rushed to merge the 1.3 patches for it. I definitely aim to have TLS 1.3 in Go 1.12.

根据惯例, Go 1.12 的发布时间将会是 2019.02~03. 如果期间你实在想用 Go 编程测试 TLS 1.3, 可以尝试使用 CloudFlare 的 tls-tris 库。根据 Go net/http 标准库维护者 Brad Fitzpatrick 的消息,这个库将会被合并到标准库作为 Go 官方 TLS 1.3 的实现。因此,如果你不得不用这个库干一些生成环境的活也大可放心,即使日后升级 Go 1.12, 接口兼容性还是有保证的。

Java 方面,由于 Java 11 出生时间好(2018.09.25), 因此是出生就支持 TLS 1.3 RFC 8446, 具体可以参见 JEP 332: Transport Layer Security (TLS) 1.3. Java 11 是 LTS 版本,因此,如果有条件升级到 11, 推荐使用 Java 11 实现的 TLS 1.3 以及配套的 HttpClient;如果生产环境暂无法升级 Java 版本,推荐使用 OkHttp. 关于 Java Http Client 选型可以参见Java HTTP 组件库选型看这篇就够了

Nginx 对 TLS 1.3 的支持

准确讲应该是 Nginx 所使用 SSL lib 对 TLS 1.3 的支持。在这方面,Boring SSL 跟进速度飞快,在 RFC 发布后第4天实现了对最终版本的支持。OpenSSL 虽然很早就跟进了 draft 的实现,但是对最终版本的支持需要 1.1.1-pre9 及以后的版本:

The OpenSSL git master branch (and the 1.1.1-pre9 beta version) contain our development TLSv1.3 code which is based on the final version of RFC8446 and can be used for testing purposes (i.e. it is not for production use). Earlier beta versions of OpenSSL 1.1.1 implemented draft versions of the standard. Those versions contained the macro TLS1_3_VERSION_DRAFT_TXT in the tls1.h header file which identified the specific draft version that was implemented. This macro has been removed from 1.1.1-pre9 and the current master branch.

TLSv1.3 is enabled by default in the latest development versions (there is no need to explicitly enable it). To disable it at compile time you must use the “no-tls1_3” option to “config” or “Configure”.

Although the latest 1.1.1 versions support the final standard version, other applications that support TLSv1.3 may still be using older draft versions. This is a common source of interoperability problems. If two peers supporting different TLSv1.3 draft versions attempt to communicate then they will fall back to TLSv1.2.

而第一个 OpenSSL 1.1.1 release 是在 2018.09.11, 因此如果你跟我一样是 OpenSSL 的死忠粉,当前阶段 Nginx 支持 TSL 1.3 的最佳方式是 Nginx 1.15.5 + OpenSSL 1.1.1. 而这种脏活、苦活、累活当然是交给 Docker 解决了:从源代码编译 nginx docker 镜像开启 TLS 1.3,项目地址可以参见 docker-nginx.

配置 Nginx 支持 TLS 1.3 需要注意一点:默认情况下 Nginx 因为安全原因,没有开启 TLS 1.3 0-RTT,可以通过添加 ssl_early_data on; 指令开启 0-RTT. 完整配置可以参考 nginx.conf.

浏览器对 TLS 1.3 的支持

当前阶段,Chrome 69 和 Firefox 62 都只支持到 draft 28, 而 draft 28 与最终版本是不兼容的。因此,要测试体验 TLS 1.3 final 需要使用 Chrome Beta 测试版。然后在 chrome://flags/#tls13-variant 开启 TLS 1.3 final:

扩展阅读

100行代码实现基于 QUIC 的 http 代理

本站开启支持 QUIC 的方法与配置后,主观感觉从国内访问快了很多。看了一下Chrome的timing, 大部分建立连接都能够做到0-RTT:

既然这样,顺手实现一个基于QUIC的http代理,把平时查资料时使用的网络也顺带加速一下。(对了,前两天看到Google发布了Outline, 看来这项运动从来都不缺少运动员哪……)

http 代理原理

http 代理处理http和https请求的方式有所不同。对于http请求:

  1. 浏览器与代理服务器建立TCP连接后,将http请求发送给代理服务器。
  2. 代理服务器将http请求发送给目标服务器。
  3. 代理服务器获取到相应结果以后,将结果发送给浏览器。

这里有一个细节需要注意,浏览器向代理服务器发送的http请求URI与直接访问有所不同。

浏览器直接访问 GET http://www.yahoo.com 的http请求格式为:

GET / HTTP/1.1
User-Agent: Quic-Proxy
...

而向代理服务器发送的http请求格式为:

GET http://www.yahoo.com HTTP/1.1
User-Agent: Quic-Proxy
...

也就是浏览器想代理服务器发送的http请求URI中包含了scheme和host,目的是为了让代理服务器知道这个代理请求要访问的目标服务器地址。

对于https请求,一般是通过CONNECT建立隧道:

  1. 浏览器向代理服务器建立TCP连接,发送CONNECT请求。
  2. 代理服务器根据CONNECT请求中包含的host信息,向目标服务器建立TCP连接,然后向浏览器返回200连接成功的响应。
  3. 这时代理服务器同时维持着连接浏览器和目标服务器的TCP连接。
  4. 从浏览器的角度看,相当于建立了一条直连目标服务器的TCP隧道。然后直接在该隧道上进行TLS握手,发送http请求即可实现访问目标服务器的目的。

QUIC Proxy的设计与实现

QUIC Proxy 部署结构图

QUIC Proxy的部署结构与上面http代理原理稍微有所不同。主要区别是增加了qpclient。主要原因是应用程序与代理服务器支架的请求是明文传输(http请求代理是全明文,https请求代理时的CONNECT头会泄露目标服务器信息)。我们是要隐私的人(虽然小扎可能并不care),因此,在应用程序与qpserver之间加了一个qpclient,之间使用QUIC作为传输层。

实现

QUIC Proxy使用Go实现,猴急的同学可以直接到github看源码:Quic Proxy, a http/https proxy using QUIC as transport layer.

代码比较简单,基于标准库的http.Server根据http代理的原理进行了一点http请求的修改。然后,因为qpclientqpserver之间使用QUIC作为transport,而QUIC上的每一个connection都是可以多路复用(multiplexing)的,因此,对于qpserver需要自己实现一个传入http.Server的listener:

type QuicListener struct {
    quic.Listener
    chAcceptConn chan *AcceptConn
}

type AcceptConn struct {
    conn net.Conn
    err  error
}

func NewQuicListener(l quic.Listener) *QuicListener {
    ql := &QuicListener{
        Listener:     l,
        chAcceptConn: make(chan *AcceptConn, 1),
    }
    go ql.doAccept()
    return ql
}

func (ql *QuicListener) doAccept() {
    for {
        sess, err := ql.Listener.Accept()
        if err != nil {
            log.Error("accept session failed:%v", err)
            continue
        }
        log.Info("accept a session")

        go func(sess quic.Session) {
            for {
                stream, err := sess.AcceptStream()
                if err != nil {
                    log.Error("accept stream failed:%v", err)
                    sess.Close(err)
                    return
                }
                log.Info("accept stream %v", stream.StreamID())
                ql.chAcceptConn <- &AcceptConn{
                    conn: &QuicStream{sess: sess, Stream: stream},
                    err:  nil,
                }
            }
        }(sess)
    }
}

func (ql *QuicListener) Accept() (net.Conn, error) {
    ac := <-ql.chAcceptConn
    return ac.conn, ac.err
}

同样的,qpclientqpserver建立连接也需要考虑到多路复用的问题,实现实现一个基于QUIC的dialer:

type QuicStream struct {
    sess quic.Session
    quic.Stream
}

func (qs *QuicStream) LocalAddr() net.Addr {
    return qs.sess.LocalAddr()
}

func (qs *QuicStream) RemoteAddr() net.Addr {
    return qs.sess.RemoteAddr()
}

type QuicDialer struct {
    skipCertVerify bool
    sess           quic.Session
    sync.Mutex
}

func NewQuicDialer(skipCertVerify bool) *QuicDialer {
    return &QuicDialer{
        skipCertVerify: skipCertVerify,
    }
}

func (qd *QuicDialer) Dial(network, addr string) (net.Conn, error) {
    qd.Lock()
    defer qd.Unlock()

    if qd.sess == nil {
        sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: qd.skipCertVerify}, nil)
        if err != nil {
            log.Error("dial session failed:%v", err)
            return nil, err
        }
        qd.sess = sess
    }

    stream, err := qd.sess.OpenStreamSync()
    if err != nil {
        log.Info("[1/2] open stream from session no success:%v, try to open new session", err)
        qd.sess.Close(err)
        sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: true}, nil)
        if err != nil {
            log.Error("[2/2] dial new session failed:%v", err)
            return nil, err
        }
        qd.sess = sess

        stream, err = qd.sess.OpenStreamSync()
        if err != nil {
            log.Error("[2/2] open stream from new session failed:%v", err)
            return nil, err
        }
        log.Info("[2/2] open stream from new session OK")
    }

    log.Info("addr:%s, stream_id:%v", addr, stream.StreamID())
    return &QuicStream{sess: qd.sess, Stream: stream}, nil
}

好吧,我承认实现代码似乎在200行左右……但是,我们实现了一个client和一个server, 平均下来基本控制在100行左右,对吧……(😂逃……)

部署

:需要golang版本 >= 1.9

1. 在远程服务器上安装 qpserver

go get -u github.com/liudanking/quic-proxy/qpserver

2. 启动qpserver:

qpserver -v -l :3443 -cert YOUR_CERT_FILA_PATH -key YOUR_KEY_FILE_PATH

3. 在本地安装 qpclient

go get -u github.com/liudanking/quic-proxy/qpclient

4. 启动 qpclient:

qpclient -v -k -proxy http://YOUR_REMOTE_SERVER:3443 -l 127.0.0.1:18080

5. 设置应用程序代理:

以 Chrome with SwitchyOmega 为例:

Enjoy!

本站开启支持 QUIC 的方法与配置

在越来越讲究用户体验的今天,网络带宽的提高已经很难有显著的页面加载改善,而低延迟的优化往往能够起到意想不到的效果。在《TLS1.3/QUIC 是怎样做到 0-RTT 的》中我们分析了TLS1.3和QUIC在低延迟方面的原理和低延迟优势。在从源代码编译 nginx docker 镜像开启 TLS 1.3中我们已经把玩了TLS1.3,没有理由不把玩一下QUIC,对吧?

起初以为,在普及程度上,QUIC因为主要是Google主导,会曲高和寡。但是,查了一下,发现腾讯早在2017年就在生产环境应用了QUIC:让互联网更快的协议,QUIC在腾讯的实践及性能优化. 结果显示:

灰度实验的效果也非常明显,其中 quic 请求的首字节时间 (rspStart) 比 http2 平均减少 326ms, 性能提升约 25%; 这主要得益于 quic 的 0RTT 和 1RTT 握手时间,能够更早的发出请求。

此外 quic 请求发出的时间 (reqStart) 比 h2 平均减少 250ms; 另外 quic 请求页面加载完成的时间 (loadEnd) 平均减少 2s,由于整体页面比较复杂, 很多其它的资源加载阻塞,导致整体加载完成的时间比较长约 9s,性能提升比例约 22%。

既然大厂都已经发车,我司也就可以考虑跟进了。稳妥起见,决定先在自己的博客开启QUIC,然后再逐步在线上业务进行推广。

方案概览

方案非常简单:不支持QUIC的浏览器依旧通过nginx tcp 443访问;支持QUIC的浏览器通过caddy udp 443访问。

由于nginx近期没有支持QUIC的计划, 作为一名gopher, 因此这里选择caddy作为QUIC的反向代理。后面会介绍caddy的具体安装和配置方法。

对于支持QUIC的浏览器来说,第一次访问支持QUIC的网站时,会有一次服务发现的过程。服务发现的流程在QUIC Discovery
有详细介绍。概括来说,主要有以下几步:

  1. 通过TLS/TCP访问网站,浏览器检查网站返回的http header中是否包含alt-svc字段。
  2. 如果响应中含有头部:alt-svc: 'quic=":443"; ma=2592000; v="39"',则表明该网站的UDP 443端口支持QUIC协议,且支持的版本号是draft v39; max-age为2592000秒。
  3. 然后,浏览器会发起QUIC连接,在该连接建立前,http 请求依然通过TLS/TCP发送,一旦QUIC连接建立完成,后续请求则通过QUIC发送。
  4. 当QUIC连接不可用时,浏览器会采取5min, 10min的间隔检查QUIC连接是否可以恢复。如果无法恢复,则自动回落到TLS/TCP。

这里有一个比较坑的地方:对于同一个域名,TLS/TCP和QUIC必须使用相同的端口号才能成功开启QUIC。没有什么特殊的原因,提案里面就是这么写的。具体的讨论可以参见Why MUST a server use the same port for HTTP/QUIC?

从上面QUIC的发现过程可以看出,要在网站开启QUIC,主要涉及两个动作:

  1. 配置nginx, 添加alt-svc头部。
  2. 安装和配置QUIC反向代理服务。

配置nginx, 添加alt-svc头部

一行指令搞定:

安装QUIC反向代理服务器caddy

上面我们提到对于同一个域名,TLS/TCP和QUIC必须使用相同的端口号才能成功开启QUIC。然而,caddy服务器的QUIC特性无法单独开启,必须与TLS一起开启,悲剧的是TLS想要使用的TCP 443端口已经被nginx占用了😂

场面虽然有点尴尬,但是我们有docker:将caddy安装到docker中,然后只把本地的UDP 443端口映射到容器中即可。

于是我们创建了一个docker-caddy项目。Dockerfile 10行内搞定:

caddy 服务配置文件/conf/blog.conf:

启动docker:

开启Chrome浏览器QUIC特性

chrome://flags/中找到Experimental QUIC protocol, 设置为Enabled. 重启浏览器生效。

测试QUIC开启状态

重新访问本站https://liudanking.com, 然后在浏览器中打开:chrome://net-internals/#quic。如果你看到了QUIC sessins,则开启成功:

当然,你也可以给Chrome安装一个HTTP/2 and SPDY indicator(An indicator button for HTTP/2, SPDY and QUIC support by each website) 更加直观的观察网站对http/2, QUIC的支持情况。

从源代码编译 nginx docker 镜像开启 TLS 1.3

nginx最近更新挺频繁的,其中TLS 1.3和是HTTP/2 Server Push是两个比较有意思的特性。前者可以有效的减少握手次数,降低延迟,尤其在恢复会话时候可以将握手开销降低到 0-RTT,后者则可以通过服务器主动推送资源,可以认为是在资源预加载之上更进一步的「资源主动加载」,有效提高网页性能和用户体验。

考虑到平时自己需要比较频繁的测试不同的nginx的新版本,但是又不想破坏机器本地已经安装好的nginx环境,因此我决定制作一个nginx docker镜像。需要说明的是,dockerhub官方是有一个nginx镜像的,但我们希望自己定制nginx版本和需要开启的模块,因此该镜像并不符合需求。

这里以开启TLS 1.3为例制作一个以ubuntu:16.04为基础镜像的 nginx dockerm镜像。顺便把上次在Ubuntu 14.04开启nginx http2支持的方法中埋的坑填了🙂

nginx开启TLS 1.3的前置条件

  1. nginx >= 1.13.0
  2. openSSL >= 1.1.1 alpha

openSSL版本有几点点需要注意:

  1. 以前可以使用分支tls1.3-draft-18编译支持TLS 1.3,但是现在已经merge到1.1.1版本中了,因此,这里不再推荐使用tls1.3-draft-18编译nginx. 使用openSSL 1.1.1编译nginx会触发nginx的一个config bug: undefined reference to 'pthread_atfork', 解决办法可以参见这里以及nginx mailing list, 这里暂时回退到使用tls1.3-draft-18.
  2. 不要尝试使用tls1.3-draft-19编译nginx, 使用Chrome 64测试无法在nginx 1.13.9上成功开启TLS 1.3, 应该跟Chrome对draft版本的支持有关系。
    • 2018-03-14更新:升级到Chrome 65以后,发现默认支持为TLS 1.3 Draft 23, 后面顺带连通config bug一起解决了再更新。
  3. openSSL目前最新版本是OpenSSL_1_1_1-pre2, 还不是stable版本,因此,不要在生产环境中使用。

Dockerfile

Nginx 开启 TLS1.3

一个完整的配置模板参考如下:

主要新增两个修改:

  1. ssl_protocols添加TLSv1.3.
  2. ssl_ciphers加入TLS13_为前缀的密码套件。

完整项目地址:docker-nginx

验证TLS 1.3

  • Chrome: 将 chrome://flags/ 中的 Maximum TLS version enabled 改为 Enabled (Draft).

如果你刚刚升级到了Chrome 65,选项中开启TLS1.3有三个选项:Enabled(Experiment 2), Enabled(Draft 22), Enabled (Draft 23). 同时很遗憾的告诉你,本文中使用的是Draft 18编译nginx,因此无法配合Chrome 65无法开启TLS1.3,请下面介绍Firefox的开启方法测试TLS1.3🤣

  • Firefox: 将 about:config 中的 security.tls.version.max 改为 4.

以Chrome 64为例,如果开启成功后Protocol显示TLS 1.3:

你也可以访问tls.liudanking.com来测试自己的浏览器是否已经成功开启TLS 1.3.

参考资料

本博客开始支持 TLS 1.3

TLS1.3/QUIC 是怎样做到 0-RTT 的

《低延迟与用户体验杂谈》中,我们提到了TLS1.3以及QUIC协议可以有效的降低传输层的延迟,改善用户体验。而TLS1.3和QUIC降低用户延迟有一个共同点,那就是在提供同等功能特性下,减少Round-Trip Time (RTT)次数。你甚至会经常听到QUIC和TLS1.3的拥趸说TLS1.3和QUIC可以做到0-RTT,基本消灭了(安全)传输层的延迟。事实真的是这样吗?

TLS RTT

我们先看一下TLS1.2 (以ECDH为例)的建立加密连接的过程:

从上图可以看出,在发送应用数据之前,TLS安全传输层需要经过2-RTT才能完成握手。如果是恢复会话,TLS1.2可以采用Session IDSession Ticket进行快速握手:

从上图可以看到,在使用Session Ticket的情况下,需要1-RTT。

那么TLS1.3是如何进一步优化的呢?

TLS 1.3完全握手

在完全握手情况下,TLS 1.3需要1-RTT建立连接。与TLS1.2有两点不同:握手过程中移除了ServerKeyExchangeClientKeyExchange, DH (Diffie-Hellman) 参数通过 key_share 传输。

TLS1.3恢复会话握手

TLS1.3恢复会话可以直接发送加密后的应用数据,不需要额外的TLS握手。因此,我们常说的TLS1.3 0-RTT握手其实是指恢复加密传输层会话不需要额外的RTT。但是,在第一次进行完全握手时,是需要1-RTT的。

与TLS1.2比较,无论是完全握手还是恢复会话,TLS1.3均比TLS1.2少1-RTT。因此,TLS1.3从被提上draft草案开始获得了各方面的好评。从nginx和Chrome支持此特性的速度可见一斑。

但是TLS1.3也并不完美。TLS 1.3 O-RTT无法保证前向安全性(Forward secrecy). 简单讲就是,如果当攻击者通过某种手段获取到了Session Ticket Key,那么该攻击者可以解密以前的加密数据。

这个问题不是TLS1.3独有,TLS1.2也存在这个问题。对于TLS1.3来说,要缓解该问题可以通过设置ServerConfiguration中的Expiration Date字段,使得与Session Ticket Key相关的DH静态参数在短时间内过期(一般几个小时)。

使用TLS最多的场景是HTTPS和HTTP/2. 以HTTP/2为例,一个完整的 HTTP Request需要经历的RTT如下:

HTTP/2 over TLS1.2首次连接 HTTP/2 over TLS1.2连接复用 HTTP/2 over TLS1.3首次连接 HTTP/2 over TLS1.3连接复用
DNS解析 1-RTT 0-RTT 1-RTT 0-RTT
TCP握手 1-RTT 0-RTT 1-RTT 0-RTT
TLS握手 2-RTT 1-RTT 1-RTT 0-RTT
HTTP Request 1-RTT 1-RTT 1-RTT 1-RTT
总计 5RTT 2-RTT 4-RTT 1-RTT

从上表可以看出:

  1. 首次连接发起HTTP请求是非常昂贵的。因此,如果你是用HTTPS作一些不可告人的代理应用的话,一定尽量保持长连接,避免频繁建立新连接。
  2. 连接的multlplexing(多路复用)可以有效减少RTT次数,降低延迟。在连接复用的情况下,TLS1.3将整个http request的RTT降低到了1个,达到理论的最小值。

QUIC RTT

  1. 当客户端首次发起QUIC连接时,客户端想服务器发送一个client hello消息,服务器回复一个server reject消息。该消息中有包括server config,类似于TLS1.3中的key_share交换。这需要产生1-RTT. 事实上,QUIC加密协议的作者也明确指出当前的QUIC加密协议是「注定要死掉的」(destined to die), 未来将会被TLS1.3代替。只是在QUIC提出来的时候,TLS1.3还没出生😂,这只是一个临时的加密方案。
  2. 当客户端获取到server config以后,就可以直接计算出密钥,发送应用数据了,可以认为是0-RTT。密钥推导可以参见Key derivation.
  3. 因此,QUIC握手除去首次连接需要产生1-RTT,理论上,后续握手都是0-RTT的。
  4. 假设1-RTT=100ms, QUIC建立安全连接连接的握手开销为0ms, 功能上等价于TCP+TLS, 但是握手开销比建立普通的TCP连接延迟都低:

    (正常体为首次建立连接的延迟,粗体部分为后续握手的延迟)

HTTP/2 over TLS 1.3 vs QUIC

当前,QUIC普遍的应用场景是 HTTP/2 over QUIC. 我们以一个完整的 HTTP Request需要经历的RTT为例,比较TLS1.3与QUIC的RTT开销:

HTTP/2 over TLS1.3首次连接 HTTP/2 over TLS1.3连接复用 HTTP/2 over QUIC首次连接 HTTP/2 over QUIC连接复用
DNS解析 1-RTT 0-RTT 1-RTT 0-RTT
TCP握手 1-RTT 0-RTT
TLS握手 1-RTT 0-RTT
QUIC握手 1-RTT 0-RTT
HTTP Request 1-RTT 1-RTT 1-RTT 1-RTT
总计 4RTT 1-RTT 3-RTT 1-RTT

从上表可以看出:HTTP/2 over QUIC相比HTTP/2 over TLS 1.3最大的优势是首次连接是的RTT开销降低了1-RTT(25%)。在连接复用的情况,QUIC的RTT开销与TLS1.3相等。

前面我们提到,QUIC(UDP+QUIC Crypto)在功能层面等价于TCP+TLS, 并且其加密协议(QUIC Crypto)未来会被TLS1.3代替。而在连接复用的情况下QUIC与TLS1.3的RTT开销旗鼓相当,那么是否意味着在HTTP/2的应用场景下,TCP+TLS1.3就可以完全替代QUIC呢?答案是否定的,主要原因如下:

  1. QUIC从数据包级别解决了队头阻塞的问题,而TCP+TLS只能解决http request级别的队头阻塞问题。具体原理可以参见《当我们在谈论HTTP队头阻塞时,我们在谈论什么》.
  2. QUIC与HTTP/2结合更加紧密,比如HTTP/2的stream, frame, Header Compression都可以直接映射到QUIC的stream, packet, Header Compression. 从协议分层看这会使得应用层和传输层紧耦合,但是在HTTP/2这个具体场景下,QUIC可以接管HTTP/2的拥塞控制,相较于HTTP/2 over TLS 1.3会受到TCP和HTTP/2两层的拥塞控制来说,无疑具有更直接有效的控制策略。并且,QUIC的拥塞控制是应用层可插件化的。虽然你也可以为TCP编写拥塞控制模块,但那是一个内核模块,稍不注意就会让系统崩溃。如果你有兴趣的话,实现一个QUIC的BBR拥塞控制模块是很容易的。
  3. QUIC支持连接迁移(Connection Migration),即在客户端切换网络IP以后,也能够保持连接不间断。这对于移动网络这种漫游网络特别友好,再加上QUIC在首次连接上RTT比TLS1.3低25%,因此会有更好的用户体验。虽然Multipath TCP, MPTCP也可以吃支持连接迁移,但这受限于操作系统和网络协议栈,要想普及,任重道远。
  4. QUIC支持Forward Error Correction,利用一定的数据包冗余,提供丢包时恢复丢包数据的能力。减少了包重传的数量,进而减少延迟,提高带宽利用率。如果你使用过kcptun,你会发现它也采用了FEC,对于弱网和高丢包环境下,效果尤其好。

当然,QUIC也是有缺点的。比如很多运营商对UDP的QoS级别是比TCP低的,甚至只允许DNS 53端口的UDP包,其他UDP包都是被封掉的。在IDC中,UDP的优先级一般也比TCP低。因此,即使以后大规模普及QUIC,也不会完全替代TCP+TLS,毕竟需要确保在QUIC不可用时,可以回落到TCP+TLS.

总结

在首次连接的时候,无论是TLS1.3还是QUIC都需要1-RTT,在连接复用的情况下,两者才能做到0-RTT. 在HTTP/2的应用场景中,QUIC可以有效的降低首次连接的RTT次数,并且支持连接迁移、FEC以及更加灵活高效的拥塞控制,因此可以在移动网络、弱网环境提供低延迟的用户体验。

扩展阅读