漫谈SNI(服务器名称指示)

漫谈SNI(服务器名称指示)
lololowe1. 为什么需要SNI
SNI(服务器名称指示)是TLS协议的扩展字段(可选的),允许客户端在TLS握手的ClientHello消息中指明目标域名,这是虚拟主机技术的基础,可以使同一IP地址托管多个HTTPS网站。
以我自己的网站来为例,我有两个域名: blog.lololowe.com 和 www.lololowe.com ,它们都指向同一个IP地址,当用HTTP协议访问这其中一个域名时,服务器会根据http请求报文中的Host字段来区分请求的网站,从而返回正确的页面
使用wireshark抓包,可以看到HTTP请求报文中的Host字段:
当使用https协议(HTTP+TLS)访问其中一个域名时,由于TLS握手(发生在会话层)是在HTTP连接(发生在应用层)建立之前进行的,因此服务器无法通过HTTP请求报文中的Host字段来决定应该返回哪个域名的TLS证书,此时就需要SNI来解决这个问题。通过引入SNI,服务器可以知道客户端请求的是哪个域名,从而返回正确的TLS证书。
在没有SNI的情况下,服务器无法在TLS握手阶段知道客户端请求的域名,因此在TLS握手完成后,服务器只能按默认顺序返回一个TLS证书,如果这个证书恰好与客户端请求的域名匹配,或者是通配符证书(一个证书对应多个域名),那么一切正常,否则,客户端会收到浏览器的证书告警,提示证书与域名不匹配。这意味着每个域名都需要有一个独立IP地址,才不会出现域名与多个证书不匹配的情况,但这样就导致了IP地址的浪费,因此SNI的出现还带来了节省IP资源的作用。
2. ESNI和ECH
2.1. 明文SNI
SNI虽然解决了HTTPS的”多个域名共享同一个IP地址”的问题,但同时也带来了隐私问题,因为SNI是明文传输的,可以使用wireshark的显示过滤器语法 tls.handshake.type == 1
来抓取TLS握手过程中的ClientHello消息,从而看到没有加密的SNI字段:
可以看到,明文SNI字段中包含了客户端请求的域名,因此,TLS握手包经过的设备(如网络日志审计系统、进行中间人攻击的黑客主机)可以轻易得知用户访问了哪些域名,从而进行审查或者偷窥和劫持。带入现实场景中举例,运营商可以知道客户何时访问了哪些网站,GFW(长城防火墙)可以通过深度包检测(DPI)SNI字段来屏蔽敏感网站的访问。
2.2. 加密SNI
为了解决SNI明文缺陷,之后TLS 1.3引入了ESNI(Encrypted Server Name Indication)协议,关于ESNI的具体工作原理,推荐看这篇cloudflare的官方博客,解释的很好: https://blog.cloudflare.com/zh-cn/encrypted-sni/
但ESNI的加密还是不够严谨(只加密SNI字段),于是又升级为ECH(Encrypted ClientHello)。通过标准化的更完整的加密,保护用户隐私。
ECH工作原理和ESNI非常类似,大致都是:客户端使用加密DNS(如DoH)查询目标域名(如 www.cloudflare-cn.com )的ECH公钥(通常存储在 _ech.www.cloudflare-cn.com 的TXT记录中),客户端使用ECH公钥加密真实SNI以及其他字段,加密的这部分被称为内部SNI(ESNI没有的),同时还会构造一个用于伪装的外部SNI(如 cloudflare-ech.com),接着客户端发送ClientHello消息到服务器,服务器用自己的私钥进行解密,提取出真实的SNI,并选择匹配的TLS证书,继续TLS握手,完成连接。如果ECH失败,客户端会回退到明文SNI。ECH具体工作原理同样推荐看cloudflare的官方博客: https://blog.cloudflare.com/zh-cn/announcing-encrypted-client-hello/
注意,如果不加密DNS查询域名的ECH配置,会显著降低ECH的隐私保护效果,因为明文查询目标域名的ECH配置,相当主动告诉监管者或者窃听者自己连接的是哪个域名。
可以使用 https://www.cloudflare-cn.com/ssl/encrypted-sni/ 来检查自己的浏览器以及网络是否支持加密的SNI:
3. SNI的发展
在国内,出境流量审查依赖于GFW,通过DPI检查SNI是否匹配敏感网站以重置TCP连接。加密SNI的出现使得GFW难以通过SNI黑名单来阻断流量,因此从2020年7月开始,GFW检测到ClientHello的encrypted_server_name
扩展字段后,会直接丢弃TCP连接,导致客户端连接失败。这说明,在网络监管与用户隐私安全的需求之间,当前更倾向于优先保障监管的需要。与此类似,IPv6在国内的推广虽有政策支持,但进展缓慢,这可能也与IPv6自带的隐私保护特性有关,IPv6地址的随机性使得追踪用户行为变得更加困难,这或许是推广缓慢的因素之一,但更主要的原因可能在于基础设施升级的成本和兼容性问题。
客户端使用加密SNI(ECH)的前提是,服务端需要通过DNS发布ECH公钥,并且用户最好通过加密DNS(如DoH/DoT)查询ECH公钥,以确保查询安全。否则,普通DNS查询可能暴露ECH使用,被防火墙(如GFW)阻断。因此,加密SNI的普及在一定程度上依赖加密DNS的推广,然而,大部分用户可能并不知道加密DNS的存在,或者因为加密DNS引入TLS握手略微增加DNS查询延迟而放弃使用,这导致了加密DNS普及率不高。
4. SNI伪装
SNI伪装,就是将SNI设为假域名(不进行加密),以此掩盖真实访问目标。例如,为了防止网管通过日志审计系统检测到自己上班摸鱼刷B站,可以伪装SNI为 blog.lololowe.com ,这样网管看到的SNI就是 blog.lololowe.com ,而实际上用户访问的是 www.bilibili.com 。Chromium系的浏览器可以使用以下cmd命令来启用SNI伪装:
1 | "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --test-type --host-rules="MAP *bilibili* blog.lololowe.com" --host-resolver-rules="MAP blog.lololowe.com 14.17.92.71," --ignore-certificate-errors |
命令中的--test-type
参数用于告诉浏览器进入测试会话。--host-rules
参数用于将 www.bilibili.com 的SNI映射为 blog.lololowe.com 。--host-resolver-rules
参数用于将 blog.lololowe.com 的IP地址映射为14.17.92.71,这个IP是从 www.bilibili.com 解析出来的,不加这个参数,会导致http请求路由到 blog.lololowe.com 的IP地址,而不是 www.bilibili.com 的IP地址,从而导致伪装无效。--ignore-certificate-errors
参数用于忽略证书错误。
命令运行后,开启wireshark抓包,接着访问 https://www.bilibili.com ,用显示过滤器语法(tls.handshake.type == 1) && (ip.dst == 14.17.92.71)
可以看到目的IP14.17.92.71的SNI为 blog.lololowe.com :
并且页面显示正常:
但是可以看到左上角有提示连接不安全,点进去查看证书,内容如下:
可以发现,证书 *.bilibili.com 是可以匹配 www.bilibili.com 的,但还是出现了证书告警,提示证书无效。复制改证书的SHA-256指纹到证书透明度网站上(crt.sh 或 platform.censys.io )进行查询,可以看到该证书是由正规CA机构颁发的受信任证书,并且尚未过期:
因此,证书本身没有问题,问题出在浏览器认为证书与域名不匹配,因为浏览器发出请求的SNI为 blog.lololowe.com 和服务器响应的 *.bilibili.com 证书不匹配,因此即使地址栏的域名和证书可以匹配,浏览器仍然会发出警告。
但是,即使用户成功伪装了SNI,日志审计系统仍然可以通过IP地址来确认用户访问了哪些网站,因为日志审计系统通常都会有一个IP地址库,用于匹配IP地址和域名。但是如果目标网站接入了CDN,那么这种情况会有所改善,因为CDN的IP地址是大量域名共享的。
最好的SNI伪装就是把SNI发送到代理服务器,由代理服务器转发到目标网站,这样日志审计系统就只能看到代理服务器的IP地址以及伪装的SNI,而看不到目标网站的真实IP和SNI。甚至可以利用这种方式实现不开任何代理和加速器,访问被GFW屏蔽的网站或者主动屏蔽大陆的网站(如netflix)。对于前者,常见的方案就是上面介绍的修改浏览器启动参数,修改域名映射规则,以及将域名解析到SNI反代服务器,该服务器可以用sniproxy或者nginx自建,也可以用网络空间资产测绘平台(如FOFA)抓取。对于后者,由于没有被GFW屏蔽,因此就不用担心SNI阻断,可以直接修改本机的host文件将域名解析到SNI反代服务器,由反代服务器中转请求。