web

WPO(Web Performance Optimization) 上

TCP, UDP和TLS

Posted by Lorry on November 26, 2018

文章字数:16633, 阅读全文大约需要:47 分钟

一直以来不敢写性能优化的东西, 但是自己写的代码越多, 调试的过程越多, 就越来越觉得性能优化是一个值得花大心思去考虑的, 加上自己也一直在致力写出更好的用户体验的代码, 总之, 先开始写吧, 一次写不完就以后分多次进行补充.

其实之前也有说过一些性能优化的东西, 比如介绍 PWA 的时候, 就有涉及部分的优化方式, 使用SW并发请求啦, 离线缓存资源啦, 还有之前说的HTTP/2的多路复用进行请求啦, 这些都是提升web性能的方面.

速度是关键

  1. 延迟: 分组从信息源发送到目的地所需的时间
  2. 带宽: 逻辑或物理的通信路径最大吞吐量

延迟是以下4种延迟类型的和:

  1. Propagation delay(传播延迟) = 传播距离/传播速度
  2. Transmission delay(传输延迟) = function(消息长度, 链路速率(bps))
  3. Processing delay(处理延迟): 处理包头(packet header), 检查位(bit-level)错误, 并且决定包的目的地花的时间.
  4. Queuing delay(排队延迟): 包排队等待处理的时间

传输延迟通常是一个不可变的, 因为传输的媒介都一样, 速度也都大致相等, 接近光速. 但是传输延迟就不一样了, 比如10Mb的文件, 1Mbps的带宽需要10s, 而100Mbps的带宽就只需要0.1s就可以传上去了.注意Bps(bytes per second)和bps(bits per second)的区别 当文件传入路由器之后, 会检查包头去决定出口路由, 并且会检查数据. 但这些都是硬件层面的, 处理延迟很小, 不过也不代表不存在.最后如果包发得过快, 路由去需要一个buffer来排队这些包, 这就是排队延迟.

每个数据包的发送都会在每个延迟上面产生很多次.

通常来说300ms用户可以感知到有点卡顿, 1000ms也就是1s用户就能感受到”等待”.所以, 必须在每个开发阶段设立明确的标准, 将延迟控制在某个范围内, 但永远也不可能消除延迟.

TCP的构成

因特网有两个核心, IP 和 TCP, TCP是负责在不可靠传输信道上进行可靠传输的抽象层, 向在它之上的应用层隐藏了大多数网络通信的复杂细节. 采用TCP可以保证发送的所有字节都能完整的被接受, 而且到达客户端的顺序也一样. 但是付出的代价就是时间控制不够好, 这也为之后进行web性能优化提出了挑战.

最著名的三次握手

实际上我认为三次握手这个说法容易有误会, 因为握手都是需要双方见面进行交流的英文原文叫 Three-Way Handshake, 跟次数好像没有关系, 仅仅是指三种不同的握手方法

  1. SYN x=rand()
  2. SYN + ACK x+1 y=rand()
  3. ACK x+1 y+1 + 应用数据

注意, 在第三个方法种的应用数据是一定要在发送完ACK之后才能发送, 因为服务器只有在接收到ACK之后才会接受应用数据.

每一个TCP建立都需要这个过程, 代价很大, 所以提高TCP性能的关键在于重用连接.

拥塞预防及控制

拥塞控制是因为IP与TCP 即传输层与数据报层之间的交互会导致拥塞, 如果往返的时间超过了所有主机的最大间隔, 那么相应的主机会在网络种制造越来越多的数据报副本. 最终所有的缓冲区都将被填满, 多出来的分组将会被删掉,

流量控制:预防发送端过多发送数据到接收端的机制

TCP每一方都需要生命自己的接收窗口 rwnd(recieve window)

当下载数据时, 浏览器rwnd可能成为性能瓶颈, 但是当上传数据时, 服务器的窗口可能成为性能瓶颈. 所以当接收的一端跟不上发送的数据时, 需要向发送端发送一个较小的rwnd.

该过程持续整个生命周期, 每个ACK分组都会携带相应的最新rwnd.

窗口缩放:

TCP的rwnd字段时16位, 最大也只能到2^16次方, 不能获得最优性能. 窗口缩放可以将2^16字节提高到1G字节(2^30), 是在三次握手时设置的, 某一个值表示将来将ACK左移16位窗口字节的位数.

慢启动

流量控制可以防止发送端不过多的向接收端过多的发送数据, 但没有机制预防任何一端向潜在网络过多发送数据. 因为在网络建立之初, 谁也不知道双方发可用带宽是多少, 而且就算知道了, 带宽也会动态的变化, 比如你在看电影, 忽然你同学也打开了看电影的网址, 这样你的可用带宽就几乎减少了一半, 那么按照之前带宽发送过来的数据对于接收方来说就太多了.那么就会在网关处堆积, 超过缓冲便会删除分组降低传输效率.

之前说了TCP在三次握手时会交换rwnd, 而服务器在握手之初会初始化一个保守的拥塞窗口(cwnd),表示发送端从客户端确认ACK之前可以发送的数据量限制.

发送端不会发送cwnd, 服务器只是维护这样一个变量, 收发双方可以发送的传输数据量(未经ACK确认的)为min(rwnd, cwnd), 在每收到一个ACK, cwnd就按2的指数倍增长.直到某个值的时候发现有丢包了, 那么就减半,再按照之前的指数增长规律增长.

0 –> 10 –> 20 –> 40 –> 80 –> 160 –> 80 –> 80 + 10 –> 90 + 10 –> 100 + 20 –> 120 + 40 一定注意后面的变化规律.

cwnd到达N所需要的时间为:(initial cwnd通常为10(之前为4))

之前说了,传输数据量的大小时cwnd与rwnd的最小值, 假设rwnd的值为64KB, RTT(往返时间)为56ms(伦敦到纽约), N = 64 * 2^10 / 1460(每段TCP的大小) = 45, 带入公式得168ms.

要想提高cwnd的增长率, 有两个方法:

  1. 缩短收发两地的距离, 这样往返时间会缩短
  2. 提高初始cwnd到10segments, (之前都是4segment, 现在基本上已经都支持10了)

对大型数据的下载慢启动不算是大问题因为会在几百毫秒之内就到达最大cwnd, 比如下载一个歌曲可能需要几十秒, 那么毫秒级的慢启动的影响就要小得多. 但是往往还有很多是小数据请求, 比如一个json, css, 可能也就几kb, 所以可能经常在还没有达到最大cwnd的时候就已经传输完成了, 无法最大化的利用TCP的传输优势. 换句话说, 收发两地的距离对于小文件的传输吞吐量至关重要.

SSR(slow-start restart)慢启动重启

在TCP被闲置一段时间后(keep-alive的长周期TCP连接), cwnd会被重置到初始值, 这是因为下次再活跃连接的时候, 网络状况可能已经发生了改变, 为了避免拥塞, 所以会重置, 但是这种情况较小, 可以对其进行禁用:(LINUX)

  • $> sysctl net.ipv4.tcp_slow_start_after_idle
  • $> sysctl -w net.ipv4.tcp_slow_start_after_idle=0

举个例子:

一个全新的TCP建立并传输一个64KB的文件需要264ms, 如果这个TCP连接可以复用呢?就只需要一个往返就够了, 可以用最大窗口进行传输.

可以看到, 带宽再也不是影响因素, 延迟和拥塞窗口大小成为了限制原因.现在已经有很多可以优化TCP拥塞控制重用连接:

  • keepalive
  • pipelining
  • multiplexing

带宽延迟积(BDP bandwidth delay product)

之前说了在达到最大的cwnd之前每次发生的数据都会比前一次指数增大, 在这种情况下, 必须停下来等待这个比前一次更大的数据被确认(因为不知道是否发生丢包), 需要等待多久? 往返时间. 这个延迟积就是 数据链路的容量 * 端到端的延迟(往返时间), 这个结果就是任意时刻处在途中未确认状态的最大数据量

假设发送端带宽10Mbit/s, 接收端100Mbit/s+, 往返时间100ms, 那么cwnd和rwnd的最小值应该设置为多少?

10Mbit/s = 10000000 / (8* 1024)= 1221KB/s 1221*0.1 = 122.1KB 之前说过最大的只能到64KB, 所以需要窗口缩放.

HOLB(head-of-line blocking)

先看看TCP已经说过的优点吧, 这些优点使TCP成为最流行的应用传输方式

  • 在不可靠通道的可靠传输
    • 包错误检测和纠正
    • 顺序传输
    • 丢包重发
    • 流控制
    • 拥塞控制
    • 拥塞预防 因为TCP会为每一个packet唯一命名并指定顺序, 如果有一个包丢失, 那么接收端会一直等待直到这个丢失的包重新到达接收端之后才会接收其他的packet, 这样就导致了线头阻塞(HOLB), 而应用时在TCP层之上的, 不知道这些具体的情况, 只能直到–哦? 有延迟? 因为这样的有序性, 所以在应用层代码中是不需要考虑数据排序的, 只要接收到数据就一定有序. 但是这是以packet到达的时间而有不可预料的延迟, 导致网络抖动.负面影响应用层的性能

有的传输并不需要TCP的所有特性, 比如顺序传输和丢包重发在某些传输里面是可以容忍的. 比如每个packet都是独立的, 并不相互依赖, 顺序也就不重要了, 并且如果每个packet都会覆盖上一个packet, 那么丢包重发也不重要了(实际上丢包是影响TCP的最大因素), 应用场景为: 视频, 游戏, 音频.遗憾的是TCP没有可配置的选项, 所以WebRTC使用UDP的原因.

TCP的优化

TCP对所有网络节点一视同仁, 具有自适应和最大限度利用底层网络的协议, 优化的最佳途径便是优化阻塞算法, 调整感知当前网络的感知方式等. 总结下TCP的关键点:

  • 三次握手
  • 慢启动
  • 流和阻塞控制吞吐量
  • 当前阻塞窗口控制吞吐量

影响延迟的关键就是缩短接收者和发送者两者的距离, 尽管现在带宽的不断提升, 但延迟受限于传播的速度(折射率已经到1.5, 很难再提高了), TCP的瓶颈都是延迟.

服务器配置调优

更新主机内核系统(往往需要升级后的代码修改)

  • 窗口缩放, 增大最大接收窗口大小
  • 增大TCP的初始阻塞窗口–initial cwnd
  • 禁用空闲时的慢启动重启
  • TCP快速打开 – TFO

应用程序行为调优

  • 发送数据越少越好
    • 减少下载不必要的资源
    • 压缩算法(gzip)
  • 不能让传输速度更快, 但是可以让传输距离更短
    • 部署CDN
  • TCP重用
    • 避免慢启动和阻塞控制影响

性能检查清单

其实就是把上述的优化整理到一起:

  • 服务器内核升级为最新版本
  • cwnd设置为10
  • 打开窗口缩放
  • 阻止空闲后的慢启动
  • 尽可能的使用TFO(需服务器和客户端都支持)
  • 避免冗余数据传输
  • 压缩传输数据
  • 缩短传输距离
  • 尽可能重用TCP

构建UDP

User Datagram Protocol, 在1980年继TCP/IP之后被加入核心网络协议. UDP可以被成为null协议, 因为简单到可以用一张餐巾纸来写完.

数据报

一个子包含, 独立的数据实体, 携带能够从源到目的地节点的路由信息而不依赖之前在节点和传输网之间的交换

注意 datagram和packet的区别, packet表示任意一种格式化的数据块, datagram通常时通过不可靠服务传输packets, 不保证送达, 没有失败提示. 这也是为什么经常会讲UDP中的U 翻译成 Unreliable而不是User.

最出名的UDP应用应该要算DNS(Domain Name System)了, 尽管浏览器本身依赖于UDP, 但是UDP协议从来没有被暴露作为一等(first class)传输而给到页面或者应用, 因为绝大多数都是TCP. 直到WebRTC的诞生.

无协议服务

要理解为什么UDP时无协议的, 首先了解一下IP, IP在TCP和UDP协议层下面一层, 负责基于地址将数据报从源到目的地主机的传输. 因此, message时包裹在IP包里的(包含源和目的地址和众多的其他路由参数).下图是20bytes的IPv4

UDP协议再把上面的message封装到它自己的结构里, 包含4个额外的字段

  • 源端口(可选)
  • 目的端口
  • 包长度
  • checksum(校验和, 可选)

因此, 当IP传输packet到目的端口时, 主机可以拆封这个UDO包, 检验应用的目的端口, 然后传输message. 完毕了, 没有更多, 也没有更少.以下是8bytes的UDP header

校验和是可选的, 因为IP包里有它自己的校验和. 所以应用能够在UDP之上进行error检测和error纠正, UDP的核心是仅仅通过嵌入源和目的端口, 提供一个 “应用复用”IP的功能. 总结以下UDP的无服务特性:

  • 不保证message传输: 没有确认, 重发, 超时
  • 不保证传输顺序: 没有包序列, 不重新排序, 没有HOLB
  • 无连接状态的跟踪: 没有连接建立或关闭的状态机
  • 无阻塞控制: 没有内建的客户端或网络反馈机制

TCP是一种面向可以通过多个没有明显限制的packets传输协议的字节流. 所以才需要两端建立一个保证这些包按需不丢失到达, 而UDP的数据报中都有明确的限制,

  • 每个数据报被单个IP包中包裹
  • 每个应用yields(生成器式)的读取完整的信息
  • 数据报不能被分片

UDP式一个简单的无状态协议, 适合在其他应用协议之上的辅助:实际上协议设计时的所有决定都是由协议上层来决定. 但是在使用自己的协议去替代TCP之前, 需要仔细想想复杂细节, 比如UDP与很多层的中间间打交道(NAT 穿透), 而且需要是一个通用的网络协议. TCP算法和他的状态机经过了几十年的磨合和改进, 并且融入了几十种并不那么容易去模仿的机制.

UDP和NAT(Network Address Translators:网络地址转换器)

IPv4只有32位长, 所以最多可以提供4.29百万个唯一ip地址. 为了解决地址被耗尽的问题, NAT被作为临时过渡方案, 在边缘网中添加NAT设备以重用IP.NAT 维护一个遍历本地IP和端口元组到一个或多个公网IP和端口元组的表.但是到后来, NAT不再是临时的解决方案, 而是网络中的基础组成部分了.

因为有了NAT, 所以有了各类的IP地址, 下面是保留的私有网络范围:

IP address range Number of addresses
10.0.0.0–10.255.255.255 16,777,216
172.16.0.0–172.31.255.255 1,048,576
192.168.0.0–192.168.255.255 65,536

为了避免路由错误, 公共主机不能被分配保留的私有网络范围IP

连接状态 超时

在 NAT 转换中UDP有一个问题是路由表必须维持去传输数据报. NAT 中间件依赖连接状态, 而 UDP 不像 TCP , 没有握手, 也没有连接终止, 没有一个很明确的连接状态可以监视.

所以NAT替UDP维护了一个表, 因为UDP没有关闭的标志, NAT只能设置延迟(Timeouts)来关闭连接, 设置的值是没有标准的, 最佳实践是每次接到一个UDP会刷新对应的时间.

虽然理论上TCP其实用不着设置超时的, 因为TCP有具体的连接状态, 但许多NAT设备都使用了同一种超时逻辑, 所以如果发现某些TCP连接被扔掉, 或许可以想想是不是NAT延时的问题.

NAT 穿透

刚刚说的不可靠的连接状态是NAT的一个严重issue, 但是在应用UDP的时候还有更大的一个issue.当建立UDP连接时, 特别是P2P应用

  • VoIP
  • Games
  • File Sharing 这些都需要客户端和服务端在peers之间都可以双向通信

但是NAT内网的设备不知道外网的IP,只知道内网IP地址, NAT在负责传输时的重写, 包括

  • UDP分组的源端口, 地址
  • IP分组的源IP地址

所以如果客户在应用程序中使用内网与外网通信是必然会失败,所以NAT穿透就没有意义了, 除非内网设备必须知道自己的外网IP.

但是知道公网IP还不足以成功传输UDP, 就像之前说的, 任何到达公网IP的NAT设备的packet必须由一个终点端口和一个table可以翻译内部的终点主机的IP和端口元组.如果表的入口不存在, 就比如是某个人想尝试型的连接以下公网, 那么这个包就被丢弃了. NAT设备就像一个简单的包过滤器, 因为它没有办法自动决定内部路由, 除非用户显式地通过端口转发(port-forwarding)或类似的机制配置过.

需要注意的是, 上述问题对客户端来说不是问题, 因为客户端会基于内部网络实现交互并且在过程中建立表的转换记录. 但是在处理有NAT的入站的来自P2P应用连接时, 还是会面对这个问题. 发明了很多穿透技术(TURN, STUN, ICE等), 在客户端和服务端都建立UDP peers之间的端到端连接活动

STUN, TURN, ICE

STUN(Session Tranversal Utilities For NAT) 可以让主机应用发现当前是否有NAT参与传输, 如果有的话就位当前连接获取公网IP和端口元组. 要完成这个需求 协议需要在公网的第三方STUN服务器的协助.

假设STUN的IP地址时已知的(不管通过DNS还是手动定义), 应用首先发送一个绑定请求到STUN服务器, 反过来, STUN用包含公有IP地址和端口来响应这个请求.这就解决了之前提到的几个问题

  • 应用层可以知道公有ip和端口
  • 出站的绑定请求可以让NAT建立路由入口
  • STUN协议可以设定一个简单的机制来keepalive 的去ping NAT, 防止NAT的超时关闭

在这个机制下, 任何时候两个peers想通过UDP进行talk, 他们都会首先向各自的STUN发送一个绑定请求, 然后得到一个成功的响应并使用得到的公网IP和端口进行数据交换.

但是, 在实践中, STUN并不足以处理NAT拓卜关系和网络参数, 在某些情况下, UDP可能会被防火墙或其他应用block掉(企业级网络很常见), 所以, 当STUN失效的时候可以使用TURN(Traversal Using Relays around NAT)协议 – 将UDP转为TCP, 其中的关键就是 Relays.

  • 双方都发送定位请求给同一个TRUN服务器, 接着进行是否允许的协商
  • 一旦协商完成, peers的通信就都把数据传到TRUN来, 然后把这个数据可靠化(转为TCP)

当然, 这样也就意味着不是P2P传输了, TRUN是在任何网络情况下最可靠的链接两个peers的方式, 但是费用高, 至少来看, 它的容量必须能够承载所有的数据流. 所以TRUN往往是作为直连失效情况下最后的保留方法.

ICE(Interactive Connectivity Establishment) 协议可以建立一个有效的NAT穿透解决方案.

  • 尽可能直连
  • 尽可能使用STUN
  • 不得已使用TRUN

优化UDP

UDP是一个面向消息的最简单的传输层协议, 可以引导其他传输协议, 再强调一遍它的特性有:

  • 无连接状态(connection stateless)
  • 没有握手(no handshake)
  • 不会重传(no retransmission)
  • 不会重排序(no reorder)
  • 不会重组(no reassembly)
  • 没有拥塞窗口和拥塞控制(no congestion window or avoidance)
  • 没有流控制(no flow control)
  • 没有可选的错误检测(no optional error checking)

你可以在自己的协议中实现上述部分功能, 但必须保证与网络中其他主机和协议的和谐.使用不当会很容易堵塞网络, 请参考列使用建议

  • 必须忍受宽范围的网络路径条件
  • 应该控制传输速度
  • 应该对所有流量有拥塞控制
  • 应该使用跟TCP相近的带宽
  • 应该准备用于重发的丢包计数器
  • 不应该发送大于路径MTU(Maximum Transmission Unit)的数据报
  • 应该处理数据报丢失, 重复或重排
  • 应该可以温度的传输, 延时两分钟以内
  • 使用IPv4的UDP校验和机制, 并且必须启用IPv6的校验和
  • 可以在需要时使用keepalives(最小15s间隔)

WebRTC就是符合这些要求的框架

TLS(Transport Layer Security)

最初是网景公司用于web上商业交易安全的, 需要哦加密客户资料和授权并且保证是安全的传输.所以 SSL 协议被加在了应用层, 在TCP之上.

当SSL被标准化后, 就被重命名位了TLS, 通常情况下两者可混用, 但实质两者的版本是有区别的.SSL是TSL的曾用名

加密, 鉴权, 数据完整性

技术上来说没必要去使用全部的三种特性, 可能决定接收证书而不去验证, 但是也要知道这样左的潜在安全风险, 在实践中, 一个安全网络应用会使用所有的三个服务

  • 加密: 混淆从主机发出数据的机制
  • 鉴权: 验证所提供验证信息的机制
  • 数据完整性: 检测message篡改(tempering)和伪造(forgery)的机制

TSL 握手

密钥协商 使用公玥加密(非对称加密), 这将可以使peers相互协商共享私玥而不用建立任何提前认知, 并且还是在非加密通道完成的.

通信两端相互校验

  • 使用浏览器时会允许客户端校验服务端是否时准备联系的那一个, 而不是其他任何通过证明名字和IP来想要伪装成终点的那一个.验证过程基于信任链 – Chian of Trust and Certificate Authorities.
  • 服务端可以选择性验证客户端, 比如公司的代理服务器能够检验所有的员工, 每个员工都有唯一的被公司签名的证书.

消息分帧机制(message frame mechanism)

  • 使用MAC(message authentication coe)签名每条信息.
  • MAC算法时单项加密的hash函数(校验和).
    • key是通过peers相互协商得来
  • 发送TLS记录时会生成MAC值并添加到消息里
  • 接收方计算并校验发送的MAC值, 确保消息完整性和鉴权

综合来看

  • 支持多种加密套件
  • 两端相互校验
  • 分帧机制下的数据完整性校验

说一下HTTPS

因为有TLS的安全性需求, 所以对已有的代理和中间设备来说(缓存服务器, 安全网关, Web加速器, 内容过滤器等)无法识别带有TLS的协议数据, 所以才有了WebSocket和HTTP/2, 基于HTTPS, 绕过中间代理, 实现可靠部署.

保护网站完整性

  • 防止入侵者篡改交换的数据
    • 重写content
    • 注入不希望且有害的内容

      保护用户隐私安全

  • 阻止入侵者监听交换的数据
    • 每个请求都会透露用户敏感信息
    • 当这些信息通过许多session聚合在一起, 可以被用来具名验证并且看到其他敏感信息
    • 所有的浏览器活动都应该考录隐私

开启了强大的功能

  • 接入用户地理位置信息
  • 拍照, 录影
  • 离线王爷体验
  • 其他共用户选择的功能 而HTTPS是用户进行授权并且保证这些数据的基础!

Internet Engineering Task Force(IETF)Internet Architecture Board(IAB)都建议开发者采用HTTPS

TSL的握手

其实跟TCP的三次握手流程差不多, 只是在ACK之后带上加密协商通道.需要确定:

  • 双方协商已知的TLS协议版本
  • 选择加密套件
  • 有必要的话验证证书

不过这些所有的步骤都需要新的包往返,会增加TLS的启动延时–2次.

下面是优化方式, 传输1个来回:

  • False Start: 当握手部分完成时开始传输加密的应用数据, 比如ChangeCipherSpec和Finished消息发送的时候, 不用等待另一方.
  • 如果客户端之前连接过服务器, 简短握手可以使用–只需要一个来回, 也减少了两端的cup计算因为直接沿用之前为安全session协商的参数

在1.3版本种, 对新的连接需要1个来回, 而之前已经连接过的, 不需要来回.

RSA Diffie-Hellman 和 Forward Secrecy

RSA时TLS部署种应用的加密方式:

  • 客户端生成一段对称key, 通过服务器公玥加密它
  • 发送到服务端以便使用对称key建立session(会话)
  • server使用私玥解密发送过来的对称key
  • key的交换完成, 两端用协商号的对称key来加密会话

RSA很好用, 但是有一个致命的缺点:

  • 同一的公私玥对是会被用作服务端的鉴权和客户端发送对称会话key的加密
  • 如果攻击者获取服务端的私玥并监听交换过程, 攻击者可解密整个会话
  • 更惨的是, 就算攻击者没有权限拿到私玥, 也可以记录加密会话, 并且在之后一旦拿到私玥之后就解密它.

相反, Diffie-Hellman 密钥交换允许两端协商一个共享的secret而不是通过显式的握手沟通:

  • 服务器私玥用来前面并验证握手
  • 建立的对称key不会再客户端或浏览器端保留
  • 不能被侦听, 就算攻击者有服务端的私玥.

可点击 Diffie-Hellman了解更多的算法详情.

最好的情况是Diffie-Hellman密钥交换方式可以减少对之前通信会话被暴露的风险

  • 生成一段新的”临时的”对称密钥作为每个密钥交换的一部分, 并且会扔掉之前的密钥.
  • 这个临时密钥绝不会被通信, 并且被每次新的会话激活协商
  • 最坏的情况是攻击者获取客户端和服务器的权限, 可以获取当前和将来的会话密钥, 但是知道这些都不能帮助攻击者解密之前的会话.

综合来看, 使用Diffie-Hellman密钥交换和临时会话密钥可以PFS(perfect forward secrecy), 可以完成一个很渴望的属性: 说得越少越好.

现在是RSA逐渐被弃用, 所有浏览器都秦相宇转发密钥的加密方式(比如通过Diffie-Hallman密钥交换), 并且还有一个顺带的好处, 可以使某种协议优化仅仅在转发secrecy时可用(比如通过TLS False Start 完成1个握手). 需要查阅服务器文档来知道如何开启并部署secrecy的转发, 好的安全性和性能总是相辅相成的.

Application Layer Protocal Negotiation(ALPN)

两个peers之间可能希望用自定义的应用协议去通信, 一种方式是决定协议的上传窗口, 使用出名的端口, 比如80给HTTP, 443给HTTPS的TLS, 并且配置所有的客户端和服务器去使用它. 但是在实践中, 这是一个缓慢并且不切实际的过程: 每个端口的配置都必须获取授权, 更糟糕的是防火墙和其他的中间设备通常只允许80和443的端口.

结果就是要使用用户协议, 必须重用80或443端口, 并且要使用额外的机制去协商应用协议. 80端口保留给HTTP并且HTTP标准对这种特殊需求提供了一些特殊的升级流. 但是Upgrade字段的使用会增加额外的网络往返延迟时间, 并且实践种通称是在中间设备间是不可靠的.

解决方法是使用443端口, 这个跑在TLS上的安全HTTPS会话, 端到端加密通道的使用混淆了中间代理的数据并且可以开启快速且可靠的方式去不是新的应用协议. 但是这依然需要另一个机制去在TLS会话中协商协议

ALPN是一个TLS的扩展, 它扩展了TLS的握手并且允许peers在不增加往返情况下协商协议. 具体的流程是:

  • 客户端添加一个 ProtocolNameList 字段, 包含在支持的应用协议列表里, 放进 ClientHello 消息中
  • 服务端检查 ProtocolNameList 字段, 并且返回 ProtocolName 字段表面选择的协议放进 ServerHello 消息中

服务端可能指响应一个协议名, 并且如果不支持任何客户端列表中的协议,服务端会选择终端连接. 结果就是一旦TLS握手完成, 安全通道便建立, 收发双方都在应用协议里了, 可以立即通过协商的协议交换消息.

Server Name Indication (SNI)

一个加密TLS 通道可以通过两个TCP peers来建立:

  • 客户端只需要知道另一端的IP地址来创建连接
  • 进行TLS握手 但是如果服务器使用同一个IP地址, 想响应多个独立站点, 每个站点都有自己的TLS证书, 怎么办?

使用SNI, 它允许客户端在TLS握手中表明客户端尝试连接的主机名. 相对应的, 服务器也可以监测在ClientHello中的SNI 的主机名, 选择合适的证书, 完成TLS握手.

TLS 会话恢复

在TLS中额外的延迟和计算成本强加给了想通过安全通信的应用一个性能上的惩罚.为了迁移这些成本, TLS提供了一种可以暂停继续的机制, 即分享在多个连接中的同一个协商密钥.

会话标识

最初在SSL2.0中被引入, 允许服务器创建并且发送一个32bytes的会话标识符作为部分的 ServerHello 消息,只要有会话ID, 收发两端都可以储存之前协商的会话参数–通过会话ID进行索引, 并且在之后的会话中使用.

具体来说就是客户端会在 ClientHello 消息中向服务端表明它依然记得之前协商的密码套件和之前握手的keys, 可以重用他们, 相应的, 如果服务器可以在它的缓存里找到ID对应的会话参数, 那么 简短握手 便会发生. 否则, 徐娅一个全新的会话协商过程, 这将会产生一个新的会话ID.

注意: 在HTTP/1.x和 HTTP/2中, 会话延续是很重要的优化功能, 可以减少很多往返的延迟和两端大量的计算成本, 实际上如果浏览器需要向同一个host发送多个连接, 它将会等待第一个TLS协商完成. 然后才打开其他连接这同一个服务器的连接. 这样他们就可以重用同样的会话参数,这也是为什么在网络trace中看不到多个相同主机的TLS协商过程.

但是一个实际的限制是需要服务端为每一个客户端创建并维持一个会话缓存, 如果每台有成千上万的唯一连接, 这会造成服务器的一些问题:因为打开每个TLS连接的内存消耗, 所以需要一个机制去合理的缓存会话ID和清除策略.

会话票(Session Tickets)

会话票是解决服务端部署TLS 会话缓存问题的, 如果客户端表明它支持会话票机制, 服务器会包含一个 New Session Ticket 的记录, 它将包含所有只有服务器知道的加密后的协商会话数据.

会话票被储存在客户端中并且能在接下来的会话请求中将 SessionTicket 包含在 ClientHello 消息中, 这样, 所有的会话数据仅仅保存在客户端, 但是票依然是安全的, 因为只有服务器能解密.

会话标识符和会话票机制也分别被称为 “会话缓存” 和 “无状态恢复机制”, 无状态恢复机制是移除了服务端的会话缓存, 通过客户端在每个新连接提供会话票给服务器的方式简化了部署, 知道票过期.

信任链和证书办法机构

其实就是一个确保加密信道两端是可悲信任的.

Alice和Bob相互信任, Charlie使用了Bob的签名, 同时获得了Alice的信任. 那么浏览器怎么知道该信任谁呢?

  • 手工指定证书, 自行导入
  • 证书颁发机构 (CA Certificate Authority)拥有者和使用者共同信赖的第三方
  • 浏览器和操作系统内置的CA名单

因为访问的站点太多, 每一个手工导入太不现实, 所以多是由浏览器代替完成, 指定可信任的根CA, 验证或撤销验证站点的工作是那些CA的事情

TLS记录协议

跟下层的IP和TCP层已有, 所有在TLS会话中的交换数据也是分帧的.

一个典型的传输应用数据流应该是:

  • 记录协议接收引用数据
  • 将数据分块, 每个记录最大2^14 bytes, 也是16KB
  • MAC(Message Authentication Code)加在每个记录中
  • 对数据用协商的加密方式加密

一旦步骤完成, 便被送到TCP层进行传输, 在接收端的顺序就反过来了.

  • 解密记录
  • 验证MAC
  • 在整条记录到达之后解压缩并且传输data到应用层

应尽可能的自行选择记录大小.每个TCP分组刚好封装一个TLS记录最佳, 一方面不要让TLS记录分成多个TCP分组, 另一方面在一条记录中尽可能多的发送数据.以下可作为参考:

  • IPv4帧需要20字节, IPv6需要40字节
  • TCP帧需要20字节
  • TCP选项需要40字节.

常见的MTU为1500字节, TLS记录大小在IPv4下为1420, IPv6下位1400, 保证向前兼容, 应采用1400字节, 当然如果MTU变小, TLS记录大小也要相应变化

升级站点内容为HTTPS

  • 混合的”活跃内容”内容, 比如通过HTTP传输的script, css会被浏览器禁止, 这会造成站点的功能异常
  • 混合的”不活跃”内容, 比如通过HTTP传输的image, video, audio等将被获取, 但是允许攻击者监视并且推断出用户活动, 还会降低性能, 因为需要额外的握手连接过程.

Content Security Policy (CSP)内容安全策略机制可以提供很大的帮助, 会验证HTTPS并且强制转到对应的策略中

Content-Security-Policy: upgrade-insecure-requests *1 Content-Security-Policy-Report-Only: default-src https:; report-uri https://example.com/reporting/endpoint *2

*1: 告诉浏览器升级所有的请求到HTTPS *2: 告诉浏览器汇报所有非HTTPS到指定位置.

性能检测清单:

  • 最佳的TCP实践
  • 升级TLS库到最新版本, 并重建与之冲突的服务器
  • 打开并配置会话缓存和无状态恢复
  • 监视会话缓存使用情况, 动态调整配置
  • 配置FSP(Forward Secrecy ciphers)开启TLS False Start
  • 关闭TLS会话关闭器去最小化往返延迟
  • 使用动态TLS记录大小优化延迟和吞吐量
  • 审计并优化证书链的大小
  • *配置OCSP(online certificates status protocol 在线证书状态协议) 封套
  • *配置HSTS(Http Strict Transport Security)和HPKP(Http Public Key Pinning)
  • *配置CSP 策略(Content Security Policy)
  • 开启HTTP/2
 $> openssl s_client -state -CAfile root.ca.crt -connect igvita.com:443

  CONNECTED(00000003)
  SSL_connect:before/connect initialization
  SSL_connect:SSLv2/v3 write client hello A
  SSL_connect:SSLv3 read server hello A
  depth=2 /C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing
          /CN=StartCom Certification Authority
  verify return:1
  depth=1 /C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing
          /CN=StartCom Class 1 Primary Intermediate Server CA
  verify return:1
  depth=0 /description=ABjQuqt3nPv7ebEG/C=US
          /CN=www.igvita.com/emailAddress=ilya@igvita.com
  verify return:1
  SSL_connect:SSLv3 read server certificate A
  SSL_connect:SSLv3 read server done A ----------------*1
  SSL_connect:SSLv3 write client key exchange A
  SSL_connect:SSLv3 write change cipher spec A
  SSL_connect:SSLv3 write finished A
  SSL_connect:SSLv3 flush data
  SSL_connect:SSLv3 read finished A
  ---
  Certificate chain -----------------------------------*2
   0 s:/description=ABjQuqt3nPv7ebEG/C=US
       /CN=www.igvita.com/emailAddress=ilya@igvita.com
     i:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing
       /CN=StartCom Class 1 Primary Intermediate Server CA
   1 s:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing
       /CN=StartCom Class 1 Primary Intermediate Server CA
     i:/C=IL/O=StartCom Ltd./OU=Secure Digital Certificate Signing
       /CN=StartCom Certification Authority
  ---
  Server certificate
  -----BEGIN CERTIFICATE-----
  ... snip ...
  ---
  No client certificate CA names sent
  ---
  SSL handshake has read 3571 bytes and written 444 bytes ----*3 
  ---
  New, TLSv1/SSLv3, Cipher is RC4-SHA
  Server public key is 2048 bit
  Secure Renegotiation IS supported
  Compression: NONE
  Expansion: NONE
  SSL-Session:
      Protocol  : TLSv1
      Cipher    : RC4-SHA
      Session-ID: 269349C84A4702EFA7 ... -----*4
      Session-ID-ctx:
      Master-Key: 1F5F5F33D50BE6228A ...
      Key-Arg   : None
      Start Time: 1354037095
      Timeout   : 300 (sec)
      Verify return code: 0 (ok)
  ---

*1: 客户端完成接收到的证书链验证 *2: 接收到的证书链(2个证书) *3: 接收到证书链的大小 *4: 为有状态的TLS恢复的会话标识符