[5] 冲浪小本本儿 (二)(2-2)----http-https-ECH 最广泛、最真实、最管用的互联网隐私保护[3]--- ECH详谈

时隔一年,Cloudflare终于再次启用加密客户端问候(ECH)功能。相关信息可参见 https://blog.cloudflare.com/new-standards/#encrypted-client-hello-ech ,同学们,本文来得太晚了
实际上,去年Cloudflare首次全面推广ECH时,我曾撰写过相关内容, https://blog.cloudflare.com/zh-cn/announcing-encrypted-client-hello .由于文章遗失,今年我将基于记忆和在使用过程中获得的新发现,再次撰写一篇相关内容。

本文将以《告别ESNI,欢迎ECH!》 https://blog.cloudflare.com/encrypted-client-hello , ECH草案 https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22
作为参考辅助,写一篇偏向工具实操为主的教程类文章
限于本人文化程度那么低,部分内容不可能像编程随想那样写深入,但也算是给编程随想未完成的内容填了一部分坑 :Firefox 开始支持 TLS 协议的【ECH 功能】 https://program-think.blogspot.com/2021/04/security-news.html

接上期讨论:“是否有办法在传递浏览器要访问的域名时,避免在SNI中暴露该信息?”加密SNI(ESNI)作为ECH的前身,最初尝试将SNI隐藏,后来进一步发展为将整个ClientHello加密。

什么是ECH?

加密客户端问候(Encrypted ClientHello,ECH)是将ClientHello分为ClientHelloOuter和加密的ClientHelloInner,从而将原本的ClientHello隐藏在ClientHelloInner中。

clientHello ---> clientHelloOuter[clientHelloInner]

sni也随之变化 原sni: private.example.com --> Outer Sni: public.example.com[Inner Sni:private.example.com]

服务器拓扑的变化

根据 IETF文档 https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni#name-topologies,传统模式与ECH的两种模式(分离模式和共享模式)如下:
传统模式

                private.example.com
client <------>      server

ECH的两种模式
分离模式

                        +---------------------+
                        |                      |
                        |   2001:DB8::1111    |
                        |                      |
Client <----->         | private.example.org |
                        |                     |
                       | public.example.com  |
                       |                     |
                       +---------------------+
                        Server
      (Client-Facing and Backend Combined)

共享模式

              +--------------------+         +---------------------+
               |                              |          |                              |
               |   2001:DB8::1111    |         |   2001:DB8::EEEE     |
Client <----------------------------->|                                |
               | public.example.com |       | private.example.com |
               |                                |       |                                 |
               +--------------------+        +---------------------+
           Client-Facing Server            Backend Server

在这种情况下,外部只能看到一个请求向IP地址2001:DB8::1111访问外部SNI public.example.com,而无法看到实际访问的内部SNI private.example.com
客户端狭义上指的是浏览器,Client-Facing Server可以理解为类似Cloudflare的CDN服务提供商,而Backend Server则是网站的源站。在实际实现中,三方需同时支持该功能。

加密ClientHello的配置

详细信息可参考 IETF文档 https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni#name-encrypted-clienthello-confi
加密ClientHello的结构如下:

struct {
    HpkeKdfId kdf_id;
    HpkeAeadId aead_id;
} HpkeSymmetricCipherSuite;

struct {
    uint8 config_id;
    HpkeKemId kem_id;
    HpkePublicKey public_key;
    HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>;
} HpkeKeyConfig;

struct {
    ECHConfigExtensionType type;
    opaque data<0..2^16-1>;
} ECHConfigExtension;

struct {
    HpkeKeyConfig key_config;
    uint8 maximum_name_length;
    opaque public_name<1..255>;
    ECHConfigExtension extensions<0..2^16-1>;
} ECHConfigContents;

struct {
    uint16 version;
    uint16 length;
    select (ECHConfig.version) {
      case 0xfe0d: ECHConfigContents contents;
    }
} ECHConfig;

在加密InnerClientHello时,需要使用HPKE公钥,该公钥存放在HpkeKeyConfig中,而HpkeKeyConfig则包含在ECHConfigContents中,最终被放置在ECHConfig内。

此外,OuterClientHello的SNI(即public_name)也包含在ECHConfig中。

ECHConfig会被放置在DNS的HTTPS类型记录中。所以也可以说,只要在解析DNS的HTTPS类型记录时能够解析出ECHConfig,就可以确认该网站启用了ECH功能。

什么是DNS HTTPS类型记录

DNS有多种记录类型,详细信息可参考 https://en.wikipedia.org/wiki/List_of_DNS_record_types 例如,类型A记录用于指向服务器的IPv4地址,类型AAAA记录用于指向IPv6地址。具体编号如下:

类型A:编号1(IPv4)
类型AAAA:编号28(IPv6)
类型HTTPS:编号65,存储HTTPS资源记录,包括ALPN、端口、IPv4提示、IPv6提示和ECH参数, https://datatracker.ietf.org/doc/html/rfc9460/#name-initial-contents
例如,记录格式如下:
svc.example.net. 7200 IN HTTPS 2 svc3.example.net. alpn=h3 port=8003 ech=...
可以在获得dns时就用apln参数约定好用http/2还是http/3访问;甚至可以从dns处自定义port参数,从而让浏览器以非443端口访问https,当然这个功能目前还没实现,应该是浏览器还没跟进;ipv4hint 这个参数挺奇葩的,浏览器本身就会发起A、AAAA、HTTPS三种记录类型查询,A查的就是ipv4,ipv4hint 也是返回的ipv4,我不是很搞得懂为什么要重复设定,莫非以后可能会取消浏览器的A、AAAA查询,只查一条HTTPS记录?
有关更多信息,请参见 https://taoshu.in/dns/dns-svcb-https.html,本文只关注ech参数
那么ech参数到底长什么样、怎么查询呢?开始进入实操环节

查看dns type HTTPS (type 65)记录获得ECH参数(echconfig)

  1. 使用dig查询DNS HTTPS记录
    linux: https://www.isc.org/download
    win: https://downloads.isc.org/isc/bind9/9.16.0/ 找到 BIND9.16.0.x64.zip下载解压

(题外话)
微软的nslookup工具多年未更新,无法查询HTTPS记录,因此需要使用Linux自带的dig工具。

hosts文件也不支持dns HTTPS记录,只支持A/AAAA记录,也就是ipv4/ipv6,这会导致hosts自定义域名 ip后,不会再去查询HTTPS记录从而导致无法使用ECH,这个问题我会再提

再说一下dig,从9.18版本开始不再提供原生 Windows 版本,9.17不是稳定版本,所以我给出的链接是9.16版本,9.17版本可以在这里下载https://downloads.isc.org/isc/bind9/9.17.0/ linux最新版已经到9.21了,希望有大佬接手编译转版本吧
9.16版本dig不支持doh和dot,这个功能要到9.18版本才支持,https://downloads.isc.org/isc/bind9/9.18.31/doc/arm/html/notes.html#id95 所以我的实验都是明文dns (plain dns)查询

回到正题,在命令行中进入BIND 9.16.0文件夹,使用以下命令查询tls-ech.dev的HTTPS记录
dig HTTPS tls-ech.dev
您使用自建DNS服务器,可以指定DNS服务器IP,自建dns服务器方法请参考 冲浪小本本儿 (七)
dig HTTPS tls-ech.dev @127.0.0.1

会返回

;; ANSWER SECTION:
tls-ech.dev.            600     IN      HTTPS   1 . ech=AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA

解析ECHConfig

要解析返回的ECHConfig,需要将以下字符串转换为十六进制(HEX):
AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
转换结果为:(google: base64 to hex)
0049fe0d00452b00200020015881d41a3e2ef8f2208185dc479245d20624ddd0918a8056f2e26af47e26280008000100010001000340127075626c69632e746c732d6563682e6465760000

00 49 条目长度 73 从fe 0d开始到结束一共73个字节
fe 0d 版本(version),和 encrypted_client_hello 这个 TLS Extension 的编码值(0xfe0d)一致。这也是曾经被人不停提起的会暴露在使用esni的字段,但现在的浏览器请求去访问网站,不论是否使用ech,这个字段都是fe0d,所以不会再出现暴露使用了ech的问题,这一点我会在下文再写
00 45 记录长度 (length),0x45 = 69 从2b 00开始到结束69个字节
HpkeKeyConfig 部分
2b config_id
00 20 kem_id
public_key 部分
00 20 长度,0x20 = 32
01 58 81 d4 1a 3e 2e f8 f2 20 81 85 dc 47 92 45 d2 06 24 dd d0 91 8a 80 56 f2 e2 6a f4 7e 26 28 - X25519 公钥
cipher_suites 部分
00 08 - 长度,0x08 = 8
00 01 00 01 - HKDF-SHA256 (0x0001) + AES-128-GCM (0x0001)
00 01 00 03 - HKDF-SHA256 (0x0001) + ChaCha20Poly1305 (0x0003)
40 - maximum_name_length
public_name 部分,服务器域名,也就是OuterClientHello的SNI
12 - 长度,0x12 = 18
70 75 62 6c 69 63 2e 74 6c 73 2d 65 63 68 2e 64 65 76 - 值,这里是 public.tls-ech.dev ,通过 hex转ascii可得(google: hex to ascii)
extensions 部分,追加的 TLS 扩展。
00 00 - 长度,0x0 = 0

参考文献:https://blog.outv.im/2023/ech/#%E6%9D%A5%E8%81%8A%E8%81%8A-echconfig

  1. 使用DoH查询DNS HTTPS记录
    2.1 用https://doh-domain/dns-query?dns=BASE64URL_OF_QUERY 方式查询HTTPS记录,本部分接续冲浪小本本(七)解读包含ECHconfig的dns 响应请求HEX文件
    手动制作一个关于HTTPS记录的DNS查询请求,构造如下:
    22 E9 01 00 00 01 00 00 00 00 00 00 不用动
    07 后面7位 tls-ech =ascii转Hex=> 07 74 6C 73 2D 65 63 68
    03 后面3位 dev =ascii转Hex=> 03 64 65 76
    00 表示域名结束
    00 01改成要查询的类型=> 00 65 https记录type id为65 =十进制转十六进制=> 00 41
    00 01 不用动

拼接后: 22 E9 01 00 00 01 00 00 00 00 00 00 07 74 6C 73 2D 65 63 68 03 64 65 76 00 00 41 00 01
转base64url: IukBAAABAAAAAAAAB3Rscy1lY2gDZGV2AABBAAE
再拼接google的doh: https://dns.google/dns-query?dns=IukBAAABAAAAAAAAB3Rscy1lY2gDZGV2AABBAAE
输入浏览器得到hex文件,用notepad++ 安装插件HEX-Editor后打开并转HEX显示
22 E9 81 80 00 01 00 01 00 00 00 00 07 74 6C 73 2D 65 63 68 03 64 65 76 00 00 41 00 01 C0 0C 00 41 00 01 00 00 00 3C 00 52 00 01 00 00 05 00 4B 00 49 FE 0D 00 45 2B 00 20 00 20 01 58 81 D4 1A 3E 2E F8 F2 20 81 85 DC 47 92 45 D2 06 24 DD D0 91 8A 80 56 F2 E2 6A F4 7E 26 28 00 08 00 01 00 01 00 01 00 03 40 12 70 75 62 6C 69 63 2E 74 6C 73 2D 65 63 68 2E 64 65 76 00 00

解析dns应答请求

22 E9 事务标识 
81 80 flags
00 01 查询请求标志
00 01 响应内容数量为1
00 00 认证
00 00 附加
07 74 6C 73 2D 65 63 68 03 64 65 76 00  - 07 代表后面有7位 , 74 6C 73 2D 65 63 68 十六进制转ascii => tls-ech  ,03 代表后面有 3位 ,64 65 76十六进制转ascii=> dev ,结合起来就是查询的域名 tls-ech.dev,也是隐藏在Innerclienthello里的sni  , 00 结束
接下来是响应的类型和其他信息:
00 41  41 16进制转10进制=>65  类型字段,表示HTTPS记录
00 01  class in
C0 0C 域名指针,指向之前的域名
00 41 返回请求类型HTTPS
00 01 class in
00 00 00 3C  dns解析结果生存时间60秒
00 52  数据长度字段,表示接下来的数据长度为82个字节
00 01  优先级(Priority)
00 
00 05 
00 4B svc参数长度75字节

接下来部分就是与dig查询得到的ech=AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA转换成hex一样的EchConfig
00 49 FE 0D 00 45 2B 00 20 00 20 01 58 81 D4 1A 3E 2E F8 F2 20 81 85 DC 47 92 45 D2 06 24 DD D0 91 8A 80 56 F2 E2 6A F4 7E 26 28 00 08 00 01 00 01 00 01 00 03 40 12 70 75 62 6C 69 63 2E 74 6C 73 2D 65 63 68 2E 64 65 76 00 00
不重复解析EchConfig

2.2 使用JSON API查询DNS HTTPS记录 本部分重复冲浪小本本(七)
您可以通过以下格式的URL使用JSON API查询DNS HTTPS记录:https://doh-domain/resolve{?name}{&type,cd,do,…}
例如,直接在浏览器中输入: https://dns.google/resolve?name=tls-ech.dev&type=65
返回:
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"tls-ech.dev.","type":65}],"Answer":[{"name":"tls-ech.dev.","type":65,"TTL":60,"data":"1 . ech=AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA"}],"Comment":"Response from 216.239.36.110."}
可以看到,返回的ech参数与使用dig查询得到的结果相同。

以下是一些支持JSON API的DoH服务提供商,您可以使用类似的查询格式:

https://dns.nextdns.io/dns-query?name=tls-ech.dev&type=HTTPS
https://dns.twnic.tw/dns-query?name=tls-ech.dev&type=HTTPS
https://dns.adguard-dns.com/resolve?name=tls-ech.dev&type=HTTPS
https://dns.quad9.net:5053/dns-query?name=tls-ech.dev&type=65
https://doh.dns.sb/dns-query?name=tls-ech.dev&type=HTTPS

cloudflare的json api使用方法请前往冲浪小本本(七)查看

国内支持JSON API的DoH服务器
https://doh.360.cn/resolve?name=tls-ech.dev&type=HTTPS


https://dns.alidns.com/resolve?name=tls-ech.dev&type=HTTPS

{"Status":0,"TC":false,"RD":true,"RA":false,"AD":false,"CD":false,"Question":{"name":"tls-ech.dev.","type":65},"Answer":[{"name":"tls-ech.dev.","TTL":60,"type":65,"data":"Not support now"}]}
阿里家不支持HTTPS记录

360家 DNS不仅支持DoH BASE64URL和DoH JSON API,作为国内老(流氓)牌服务商,甚至没有主动阉割HTTPS中的ECH参数,这让我感到意外。
阿里家则直接阉割了整个HTTPS记录,可是HTTPS记录里又不光只有ECH参数,HTTPS记录里还有alpn port ipv4hint ipv6hint参数。但通过阿里家的DoH BASE64URL测试,我仍然能够获得HTTPS记录,这让我感到困惑。
这也意味着,未来如果GFW 要阻止ECH,国内的DoH服务商可能会主动阉割整个HTTPS记录,或者精准阉割ECH参数。

API地址格式的多样性
目前,DoH JSON API的地址格式尚未统一,有些使用:
https://doh-domain/dns-query?name=query-domain...
而有些则使用:
https://doh-domain/resolve?name=query-domain...
例如,nextdns 非常宽泛两种都支持,而cloudflare还需要设置http head,因此想要测试一家doh服务商是否支持json api要两种格式都试试,都不灵再去看看这个服务商的帮助文档

  1. 在线DNS解析网站查询DNS HTTPS记录
    尽管有众多网站提供DNS解析查询服务,但支持查询DNS类型HTTPS记录的网站相对较少,甚至比支持DoH JSON API的网站还要稀缺。目前,我仅收集到以下几家,希望未来能有更多在线DNS查询平台支持类型HTTPS(65)记录的查询。
    google家,只能说google家服务确实强大,各种支持
    https://dns.google/query?name=tls-ech.dev&rr_type=https&ecs=
    DNS Name 填入域名,RR Type填入HTTPS或者编号65

nslookup.io家
https://www.nslookup.io/domains/tls-ech.dev/dns-records/https/

gcore家
https://gcore.com/dev-tools/dns-lookup

三种查询方式里,我们认为在线网站解析dns总是可靠的,国外的doh商是可靠的但会有SNI阻断问题,国内的doh商的源头可能已经自己主动污染结果不一定可靠,明文dns查询是最不可靠但能够反映出具体的网络环境

客户端如何获得EchConfig

在讨论如何通过不同渠道查询DNS HTTPS记录以确认某一网站是否启用ECH后,接下来将探讨client这一端如何获得echconfig
client可以是手机应用程序或翻墙工具,那么就可以先预置在里面。例如,客户端可能会内置ECH配置,可参考以下文档: For example, the client may have the ECH configuration preconfigured. https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni#name-encrypted-clienthello-ech
更多的是不预置echconfig、client是浏览器的情况,需要浏览器通过dns查询去获得dns https记录里的echconfig。浏览器能否使用ech,关键就在于浏览器能否获得这串echconfig




只要不是很老的浏览器,在访问一个网址前,都会发起三条进行dns查询,A、AAAA、HTTPS记录类型查询,这一现象可以通过自建本地DNS服务器进行观察。有关如何自建AdGuard Home DNS服务器的详细信息,请参见冲浪小本本儿(七)
从图中可以看出,www.xuexi.cn的HTTPS记录请求 ,响应的是一个CNAME记录
www.cloudflare.com的HTTPS记录请求,响应了HTTPS记录,但是没有包含ech参数
tls-ech.dev 的HTTPS记录请求,响应了HTTPS记录,同时包含了ech参数,此时浏览器获得ech参数可以开始使用ech方式连接
到了这一步,不得不再次引入一次完整的网站访问所经历的流程来说明为什么ECH推荐搭配DOH使用

浏览器访问网站完整流程

在明文dns查询情况下,无ECH访问(plain dns + plain sni)

www.xuexi.cn A ?
www.xuexi.cn AAAA ?
www.xuexi.cn HTTPS ?
client  ----------DPI--------------->运营商DNS服务器
          <--------DPI---------------- www.xuexi.cn 49.7.23.123
sni: www.xuexi.cn
          ----------DPI--------------->🐷  49.7.23.123
  1. 用户在chrome地址栏中输入 https://www.xuexi.cn 。浏览器需要获取该网站的IP地址,因此会依次检查浏览器缓存、系统hosts文件和系统DNS缓存。如果未找到对应的IP地址,浏览器将向系统指定的DNS服务器发起明文DNS A、AAAA和HTTPS记录的查询请求。
    系统指定的DNS服务器在Windows系统中可以通过控制面板的“网络和共享中心”查看,路径为“以太网” → “详细信息” → “IPv4 DNS 服务器”和“IPv6 DNS 服务器”。在Linux系统中,可以通过命令 cat /etc/resolv.conf 查看。
    通常,这些DNS服务器指向网关DNS服务器,而网关DNS服务器会向上游的运营商DNS服务器发起查询请求。
  2. 运营商DNS服务器通过迭代或递归查询获取 www.xuexi.cn 的A记录,即IP地址 49.7.23.123。同时,AAAA和HTTPS记录也会返回给网关DNS服务器,随后网关DNS服务器将这些信息返回给浏览器。
  3. 浏览器检查HTTPS记录,发现未包含ECH参数,因此向 49.7.23.123 发送包含SNI :www.xuexi.cn 信息的ClientHello连接请求。完整的HTTPS握手过程可参考《冲浪小本本儿(二)(2-1)

在此过程中,有两个环节可能会暴露用户隐私:首先,在向运营商DNS服务器发起DNS查询时,明文DNS查询会被深度包检测(DPI)所监视;其次,在向 49.7.23.123 发起ClientHello请求时,SNI信息也会被监测。

在明文dns查询情况下,有ECH访问 (plain dns + ech)

tls-ech.dev A ?
tls-ech.dev AAAA ?
tls-ech.dev HTTPS ?
client  -----------DPI-------------->运营商DNS服务器
          <---------DPI--------------- tls-ech.dev  34.138.246.121  ech=...
OuterSni: public.tls-ech.dev[InnerSni: tls-ech.dev]
          -----------DPI--------------> 34.138.246.121
  1. 用户在chrome地址栏中输入 https://tls-ech.dev ,浏览器想要访问 tls-ech.dev 需要ip地址,在依次寻找浏览器缓存—>系统hosts—>系统缓存 没有找到对应ip后,开始向系统指定的dns服务器发起明文dns A\AAAA\HTTPS记录查询请求,系统指定的dns一般是局域网内的网关dns服务器,网关dns服务器又会向上游的运营商DNS服务器发起查询请求
  2. 运营商DNS服务器 经过迭代或递归查询得到 tls-ech.dev 的A记录ip 49.7.23.123 , AAAA\HTTPS 记录 返回给网关dns服务器,网关dns服务器又返回给浏览器
  3. 浏览器 这次从HTTPS记录里得到了echconfig,向34.138.246.121 发送OuterclinetHello sni: public.tls-ech.dev ,InnerClientHello sni:tls-ech.dev 的连接请求

此时有一处地方会暴露隐私,在向运营商DNS服务器发起DNS查询时,明文dns查询会被DPI看到。DPI只能看到向34.138.246.121 发起clienthello sni: public.tls-ech.dev的请求,看不到实际访问的是 tls-ech.dev
尽管如此在草案上没有禁止在明文dns下使用ECH

https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni#name-unauthenticated-and-plainte

wherein DNS Resource Records are signed via a server private key, ECH records have no authenticity or provenance information. This means that any attacker which can inject DNS responses or poison DNS caches, which is a common scenario in client access networks, can supply clients with fake ECH records (so that the client encrypts data to them) or strip the ECH record from the response. However, in the face of an attacker that controls DNS, no encryption scheme can work because the attacker can replace the IP address, thus blocking client connections, or substitute a unique IP address which is 1:1 with the DNS name that was looked up (modulo DNS wildcards). Thus, allowing the ECH records in the clear does not make the situation significantly worse.

其中 DNS 资源记录是 通过服务器私钥签名,ECH 记录没有真实性或出处 信息。这意味着任何可以注入 DNS 响应或 毒性 DNS 缓存(客户端访问网络中的常见情况)可以 向客户端提供虚假的 ECH 记录(以便客户端对数据进行加密) 或从响应中去除 ECH 记录。然而,面对攻击者 控制 DNS,则没有加密方案可以工作,因为攻击者可以 替换 IP 地址,从而阻止客户端连接,或将 唯一 IP 地址,与查找的 DNS 名称 1:1(模 DNS 通配符)。因此,允许 ECH 记录以明文形式显示并不会使 情况明显恶化。

HTTPS记录也可以像A记录(ipv4)一样被GFW抢答污染,但草案给出宽泛的使用条件, 这不会使情况变得更糟,最坏也是隐私性退回到“明文dns查询情况下,无ECH访问”,即GFW能通过明文DNS猜测这个sni:public.tls-ech.dev的clientHello 连接请求正在访问tls-ech.dev
也因为明文dns下也能使用ECH,理论上,网站升级到ech对用户来说是无感的

在加密dns(doh/dot/doq)查询情况下,有ECH访问 (doh + ech)

tls-ech.dev A ?
tls-ech.dev AAAA ?
tls-ech.dev HTTPS ?
    ↓  
dns.alidns.com A ?
dns.alidns.com AAAA ?
dns.alidns.com HTTPS ?
client----------------DPI---------------------->运营商dns服务器 
                 <--DPI---------------------- dns.alidns.com 223.5.5.5
dns.alidns.com  tls|/dns-query=tls-ech.dev&...|
         ----------------DPI---------------------->阿里doh服务器 223.5.5.5         
              <-------DPI-----------------------tls |tls-ech.dev 34.138.246.121 ech...|
public.tls-ech.dev  tls|host:tls-ech.dev|
        ----------------DPI-----------------------> 34.138.246.121
  1. 用户在chrome地址栏中输入 tls-ech.dev ,浏览器想要访问 tls-ech.dev 需要ip地址,在依次寻找浏览器缓存—>系统hosts—>系统缓存 没有找到对应ip后, 开始查询浏览器指定的doh服务器的ip,举例为 dns.alidns.com (设置方法我会在下文给出), 向运营商dns服务器发起明文dns A\AAAA\HTTPS查询doh的地址
  2. 运营商dns服务器返回doh的地址
  3. 浏览器向DoH服务器发起针对域名 tls-ech.dev 的加密DNS A、AAAA和HTTPS记录查询。
  4. doh服务器返回 tls-ech.dev的A\AAAA\HTTPS记录
  5. 浏览器 从HTTPS记录里得到了echconfig,向34.138.246.121 发送OuterclinetHello sni: public.tls-ech.dev ,InnerClientHello sni:tls-ech.dev 的连接请求

在DOH+ECH的设计下,外部就只能看见发向对运营商dns服务器的对doh域名的明文dns查询请求,访问doh服务器时的sni,以及OuterClientHello的sni,多了一个来回以及加密解密的开销,但这显著提升了我们的互联网隐私,自TLS加密HTTP以来,这一设计再次推动了隐私保护的进步,完善了隐私保护的最后一环。

加密dns查询,无ECH访问 (doh + plain sni)

这种情况就不赘述了,这种情况我在冲浪小本本(七)已经写过了,通过DoH获取正确的IP地址本身就是一种清理DNS污染的方法,当然在发起连接请求时,ClientHello的SNI仍然会被GFW监测到,但由于DNS污染名单和SNI阻断名单不同,所以被DNS污染、不被SNI阻断的网站还是能通过任意方式获得正确的ip来顺利连接


client---家庭网关/寝室路由器/通信塔...---电信/移动/联通运营商交换机---....骨干广域网路由...---云托管厂商--->🐷  49.7.23.123

这里使用DPI仅是为了便于描述,数据包在网络上传递时,局域网网关、运营商、公网路径上的路由器以及GFW等都能够看到明文DNS查询和ClientHello的SNI,ECH隐私保护的目标就是防止在Client与Server之间的网络中任意节点或设备看到数据包访问的实际域名。虽然ECH能够隐藏SNI,但并未隐藏IP地址,因此在遇到路由黑洞时仍然无法解决这一问题,关于这一部分,我将另行撰写一篇文章进行深入探讨。

检查浏览器是否支持ECH

ECH需要三端同时支持,暂时将Client-Facing Server 和backend server视为一体为网站方,确认网站方是支持的情况下,如果浏览器无法以ECH方式访问,那么就能确认是浏览器、或者是浏览器环境的问题

  1. 通过访问 https://tls-ech.dev
    显示 You are not using ECH. :( 没有在使用ECH访问
    显示 You are using ECH. :) 在使用ECH访问

  2. 通过访问 https://defo.ie/ech-check.php

SSL_ECH_OUTER_SNI: cover.defo.ie
SSL_ECH_INNER_SNI: defo.ie
SSL_ECH_STATUS: success ✔

在使用ECH访问

SSL_ECH_OUTER_SNI: NONE
SSL_ECH_INNER_SNI: NONE
SSL_ECH_STATUS: error getting ECH status ❌

没有在使用ECH访问

  1. 通过访问 https://www.cloudflare.com/ssl/encrypted-sni/
    查看“Secure SNI”是否 :heavy_check_mark:

  2. 对于托管在 cloudflare的网站,在域名后面加上 /cdn-cgi/trace ,例如 https://konachan.com/cdn-cgi/trace
    如果显示 sni=encrypted,说明当前正在使用 ECH访问
    如果显示 sni=plaintext, 说明当前正在使用明文SNI访问

我分别在Edge、Chrome和Firefox浏览器中测试了在明文DNS和使用DoH时访问上述网站以检测ECH的支持情况。


edge
使用明文dns 设置方法:设置 - 使用安全的 DNS 指定如何查找网站的网络地址 直接关闭
使用doh 设置方法:设置 - 使用安全的 DNS 指定如何查找网站的网络地址 打开 请选择服务提供商—选择四大doh或自己填入可用的doh
由于默认四大doh在国内极大可能不可用,请自行寻找可用的doh服务器,不可用的情况就是显示红字“请验证这是否是有效的服务提供商”,同时在网页地址栏输入必然可到达地址例如www.xuexi.cn访问,网页提示 DNS_PROBE_STARTED


chrome
使用明文dns 设置方法:设置 - 隐私和安全-安全-使用安全 DNS 关闭
使用doh 设置方法:设置 - 隐私和安全-安全-使用安全 DNS 打开 选择四大doh或“添加自定义DNS服务器提供商”
同理,由于默认四大doh在国内极大可能不可用,请自行寻找可用的doh服务器,需要注意的是,Chrome在四大DoH不可用的情况下不会提供提示;自定义doh不可用会提示红字“请检查并确保这是一个有效的提供商,或者稍后重试”


firefox
使用明文dns 设置方法:设置 - 隐私与安全-"基于 HTTPS 的 DNS"启用策略 - 关闭
使用doh 设置方法:设置 - 隐私与安全-"基于 HTTPS 的 DNS"启用策略 - 最大保护 填入可用的doh服务器地址

ECH成功  edge  chrome  firefox
明文dns   | 时灵时不灵|    ×
doh        |✔ |  ✔|    ✔

在测试过程中,切换明文DNS和DoH后,必须清空浏览数据、重启浏览器,否则可能会出现同一浏览器在访问 tls-ech.dev 时显示未进行ECH访问,而在访问 defo.ie 时显示正在进行ECH访问。

最终我得到了上述结果,尽管edge和chrome在明文dns下也会发起A\AAAA\HTTPS记录查询,但是ECH 时灵时不灵,往往是第一次连接不能启用ECH,关闭浏览器再连接就能启用,很奇怪,我也找不到原因,上面就谈过草案并没有禁止在明文dns下进行ECH访问。而在启用DoH后,ECH的使用则变得稳定。

至于firefox ,尽管Firefox称使用ECH时只是建议开启doh, ECH delivers the most privacy benefit when DNS records are fetched via an encrypted transport like DoH, so we recommend enabling DoH in Firefox. https://support.mozilla.org/en-US/kb/understand-encrypted-client-hello
但是根据实测,不开启DOH根本就无法使用ECH,实际上firefox执行的是MUST enabling DoH ,也是收紧了草案的政策

出于好奇,我想知道firefox是如何禁止在明文dns下使用ECH的,我将系统dns 设置到 自建dns服务器,查看dns日志后,发现firefox在关闭doh的情况下,居然是不会进行HTTPS记录查询的,只有A\AAAA记录查询,那这就完全说得通了,关闭doh时firefox根本就无法获得echconfig,那肯定不会进行ECH连接了,只是我没想到它会以如此粗糙的方式来禁止在明文dns下使用ECH的

综合上述情况,一是为了安心的使用ECH,二是为了访问被DNS污染、没有被SNI阻断的网站,建议在浏览器中主动启用DoH,哪里去获得这些doh地址请参看冲浪小本本儿 (七),测试DOH地址可用性我会作为一个部分放入冲浪小本本儿 (七)(7-1)。

在国内公网环境中可用的doh地址被污染例如onedns这些国内doh商,没被污染的被GFW SNI阻断例如默认的四大doh商,,可以通过自建AdGuard DNS服务器并使用自签证书来为浏览器提供DoH服务,将浏览器的DoH地址指向本地自建的AdGuard DoH服务器来,从而变相地将浏览器doh转换成AdGuard 的 dot/doq/h3各种方式的上游加密dns。而连接各种方式、地址的上游加密dns突破GFW针对部分doh域名的SNI阻断的可能性就高的多了,我会尽快完成 冲浪小本本儿 (七)(7-1) adguard home自签证书让局域网内浏览器用上doh

在代理情况下,chrome 无法使用ech,但firefox反而可以完美地使用ECH,限于篇幅,我会单独开另一篇再写

查看与当前网站连接是否启用了ECH

通过访问 tls-ech.dev 只能确定自己的浏览器是支持ECH的,那么有没有办法让浏览器提示用户当前正在访问的网站正在使用ECH呢?就像在使用tls加密时,地址栏会出现一把小锁。上次撰写这篇文章时,我提到过这一缺陷,到今天各大浏览器厂商也没跟进,倒是给我找到一个扩展解决了。
我是在这个issues里 https://github.com/net4people/bbs/issues/393#issuecomment-2373136529 找到了一个firefox的ECH显示扩展:https://addons.mozilla.org/zh-CN/firefox/addon/oh-my-ech/ 项目地址:https://github.com/27justin/ohmyech

该扩展的状态指示如下:

红色的	此选项卡中从未使用过 ECH。
绿色的	来自此选项卡的每个请求都使用 ECH(最佳情况)。
蓝色的	初始页面加载(第一个请求)使用了 ECH,但后续提取却没有使用。
橙子	初始页面加载未使用 ECH,但后续提取使用了。

我很好奇,浏览器扩展应该只能看到地址栏中的域名和HTTP头中的Host字段,它不应该能够看到Outer ClientHello的SNI。例如,在地址栏中输入 https://www.qbittorrent.org 后,HTTP头中的Host为 www.qbittorrent.org,扩展可以看到这一信息,但Outer ClientHello的SNI(如 cloudflare-ech.com)则是扩展无法访问的。因此,我想知道它是如何判断的。

我翻了一下这个插件的源码 https://github.com/27justin/ohmyech/blob/ec7935d500a9d354776586e25261cef3595c40c7/background.js#L41
找到了检查使用ECH判断的关键代码 const usedECH = tls.usedEch;
我进一步搜索了 tls.usedEch ,在firefox文档中找到了 tls.usedEch https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/SecurityInfo#usedech
如果 usedEch 为true,则表示该请求使用了ECH;反之,则未使用。原来这个扩展是直接调用 tls.usedEch 的值来判断某一连接是否使用了ECH,而不是通过对比Outer SNI得出的。

那么 tls对象是如何判断某一条连接是否使用了 ECH 从而给tls.usedEch赋值呢?就不翻firefox源码了,睡大觉。
可惜的是目前只有firefox支持这个插件。

经过一系列的步骤,我们也知道了如何进行简单的排查ECH使用中的问题

  1. 先确认要访问的网站是否支持ECH,先用dig明文查一下是否包含ECH参数,如果有,进入第2步
    如果没有,用在线网站查一下,在线网站查到存在后,进入第2步。同时这也说明明文dns的https记录被污染了,成因具体原因具体分析。
    如果在线网站里也查不到ECH参数,说明这个网站没有开启ECH
  2. 确认网站支持ECH后,浏览器访问 tls-ech.dev,检查浏览器是否支持ech,不支持就开启doh再试试
  3. 双端都支持ECH下就能正常使用ECH方式访问了

新的问题,我能通过访问上述网站来确认网站是否开启了ECH和正在使用的浏览器是否支持ECH,甚至能通过firefox的小插件来确认当前页面是否正在使用ECH来访问,但无法得知当前正在使用的ECH的OuterClientHello的SNI是什么,因此,我引入了抓包工具来查看这一信息

通过wrieshark 抓包 ECH

实验环境

  1. 自建 adguard 服务器并自签证书启用doh
  2. chrome
  3. wireshark 用tls.handshake.type==1来过滤获得clienthello的握手包
       wireshark
           ↓
chrome -------> adguard dns
        <------
       --------> website

实验一 在明文dns查询情况下,进行ECH访问



在首次访问时,无法使用ECH访问,但是在日志里能够看到有A/HTTPS查询记录的。没有AAAA查询记录是因为没有IPv6公网,换一个有IPv6的环境就有了,这并不影响实验结果。

在重启浏览器后就能够进行ECH访问了

实验二 在浏览器开启doh情况下,进行ECH访问


在开启DOH的情况下,会先进行DOH地址的解析,ECH访问也不会出现明文下失灵的问题,通过日志可以清晰地看到使用的是DoH的查询方式

总结:
通过实验一,我们可以清楚地看到,在没有ECH的情况下,SNI就是输入浏览器地址的 tls-ech.dev,在ECH的情况下SNI才是与浏览器地址不同的public.tls-ech.dev ,而tls-ech.dev 会被加密在 Extension: encrypted_client_hello 字段下的 Payload里面
但由于我无法解密Payload,所以也就无法直接证明这个sni: public.tls-ech.dev的clientHello 中存在一个sni:tls-ech.dev的InnerClientHello


只能通过抓到的 ip 相同 所以是同一个数据包,sni与域名不同推测它包含了一个InnerClientHello 。希望浏览器厂商能够尽早跟进,提供用户显示当前连接是否使用ECH的功能,并在使用时显示Outer ClientHello的SNI。

深入探讨Cloudflare对ECH的支持

Cloudflare在ECH(Encrypted Client Hello)方面的支持力度相当显著,值得我们深入了解。根据Cloudflare的官方文档 https://developers.cloudflare.com/ssl/edge-certificates/ech/ ,Cloudflare作为面向客户端的服务器,其Outer SNI 为cloudflare-ech.com

然而,如何找到托管在Cloudflare下并启用ECH的网站呢?在 https://github.com/net4people/bbs/issues/393#issuecomment-2365428913 有热心网友分享了一些相关网站,期待有更多人能够整理出一个完整的列表。

  1. 检查网站的EchConfig
    首先,我们可以查看一些网站的EchConfig,例如:
    https://dns.google/query?name=www.qbittorrent.org&rr_type=https&ecs=
    https://dns.google/query?name=rutracker.org&rr_type=HTTPS&ecs=
    https://dns.google/query?name=servo.org&rr_type=HTTPS&ecs=

通过这些查询,我们发现它们共享一个EchConfig:
AEX+DQBBsQAgACAm84FhkZGIgRHpeXEGS20IcTaJhYEbIvm9YvwcSA4QYwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA=

tips,如果不想像上面那样逐字节解读,只想知道这串EchConfig 里的 public_name直接 转ascii就行了(google: base64 to ascii)
EþA±  &ó�a����éyqKmq6���"ù½büHccloudflare-ech.com
可以直接看到配置的outerclienthello的sni: cloudflare-ech.com

  1. 抓包分析Cloudflare下支持ECH网站的Outer SNI
    由于我比较倾向对用户无感的ECH访问,所以优先使用明文的dns查询+ECH访问。 我以https://www.qbittorrent.org 为例

之前就说了,明文dns使用ECH之前会有一次没有ECH的访问,这次也观察到了,就不截图了
再次访问就能观察到访问域名是www.qbittorrent.org,而显示sni是 cloudflare-ech.com了,而http3 是使用QUIC承载的,这点在抓包和F12面板上也能得到验证

降级HTTP3到HTTP2

由于国内存在QOS限流,UDP承载的数据包在传输量过大时可能会受到限制,也就是可能h3用着用着就卡了,因此HTTP/3可能会出现卡顿现象。为了解决这个问题,可以选择关闭QUIC,改用TCP承载的HTTP/2连接。

关闭QUIC的方法如下:
在Chrome浏览器中输入chrome://flags,找到QUIC选项并切换为“disable”。

如果在使用过程中没有出现卡顿现象,则无需更改此设置,以上仅为预设解决方案。

通过这次实验我们也能看出,cloudflare通过将域名同一化成 cloudflare-ech.com 来给backend 服务器提供保护,它提供了一种互联网场景就是,所有https流量的数据包的sni都变成了cloudflare-ech.com,实际访问的却是 www.qbittorrent.org、www.xuexi.cn... 来保护隐私。
理论上,托管在cloudflare-ech.com下的网站越多,每个网站的安全性就越高。假设有n个网站托管在Cloudflare下,外部猜测正在访问的SNI的概率为1/n。缺然而,这种集中化的做法也存在风险:如果GFW 直接阻断了cloudflare-ech.com的SNI那么所有托管在其背后的网站都将无法访问,即便这些网站原本不在GFW的SNI阻断名单里。而这一点已经在俄罗斯出现了 https://therecord.media/russia-blocks-thousands-of-websites-that-use-cloudflare-service ,目前不知道是识别并精准封锁了所有ECH流量,还是把sni为 cloudflare-ech.com的流量封锁了,目前找不到更多跟进信息,但我倾向后者

对于这种将所有“鸡蛋放在同一个篮子里”的做法,我不想做过多评价。从技术角度来看,任何人都可以自建Client-Facing Server和Backend Server。许许多多单人自建的ECH网站是否会比集中使用同一个Outer SNI的情况更好,这仍然是一个值得探讨的问题。

在客户端禁用ECH

https://therecord.media/russia-blocks-thousands-of-websites-that-use-cloudflare-service 报道可以看出,俄罗斯正在尝试让更多网站在服务器端主动禁用ECH,但事实上不用,客户端也能主动禁用ECH

  1. 最简单的使用firefox 关闭DOH
    这算是一个取巧的方法,我们上面就写过,firefox在关闭DOH的情况下,明文DNS查询是不会进行dns HTTPS记录查询的,拿不到echconfig那肯定无法使用ech了
    这种方法的缺点在于,如果明文DNS查询的域名A记录被DNS污染,用户将无法获得正确的IP地址。为了确保能够正确获取IP,最佳做法是启用DoH,但启用DoH又会导致查询HTTPS记录,从而重新启用ECH。因此,这种方法需要确保明文DNS查询能够获得正确的A记录。

  2. 自建dns服务器,重写DNS响应(1)
    以adguard为例,在“过滤器”-“自定义过滤规则”里写入你要重写的域名的A记录,不写入HTTPS记录,那么返回HTTPS记录是空的
    这里以 tls-ech.dev 为例,同时在浏览器启用doh以保证可以稳定进行ECH,写入
    ||tls-ech.dev^$dnsrewrite=NOERROR;A;34.138.246.121


    此时返回的HTTPS记录是空的

    用这个方法之前要先查日志,看一下要访问的域名的IP是什么才能再写
    ||域名^$dnsrewrite=NOERROR;A;x.x.x.x
    或者
    ||域名^$dnsrewrite=NOERROR;AAAA;x:x:x::x
    这其实就是一种DNS污染

  3. 自建dns服务器,重写DNS响应(2)
    adguard 其实还有个更简单的 “过滤器”-“重写DNS”,添加DNS重写,依次填入域名、IP ,同样也是不会查询HTTPS记录并传回给浏览器的,原本是缺点,在这里到变成一个tips了

  4. 通过系统hosts文件重写域名 ip映射
    这也原本是我在使用ECH时遇到的问题,假如Hosts文件手动重写域名 ip映射,那么浏览器将不会再进行HTTPS记录查询,也等于变相禁用了ECH
    https://v2ex.com 为例,v2ex.com是在SNI阻断名单上的,同时v2ex.com也托管在cloudflare下并启用了ECH
    假如我直接访问 v2ex.com ,是可以连接的,因为启用了ECH
    假如我在hosts重写了 104.20.48.180 v2ex.com ,就访问不了了,因为没有启用ECH连接 SNI暴露了,又重新被SNI阻断了,我就不抓包截图展示了
    因此希望windows和linux可以在hosts跟进设置HTTPS记录

关于ECH的特征

曾经的ESNI会把扩展标识encrypted_server_name 设置为 0xffce https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-02#section-5 ,这也是一直被人诟病的明显特征,由于我目前无法访问启用ESNI的网站,因此无法进行抓包验证。

在GFW掌握了这个特征了之后就开始对ESNI审查 https://gfw.report/blog/gfw_esni_blocking/en/#the-gfw-censors-esni-but-not-omit-sni “encrypted_server_name 扩展的 0xffce 扩展标识是触发封堵的必要条件”

不同于以往对http https的rst注入

client          GFW        server
clientHello--->|-------->
|<------------rst
|time|<------------------

GFW 是能直接丢弃数据包的,此时无法像RST注入那样有回包,数据包根本到不了server,没有连接可能

client               GFW                server
clientHello|0xffce|--->×

随着ESNI的迭代到了ECH版本,这一字段也变成了encrypted_client_hello设置成了0xfe0d https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni#name-the-encrypted_client_hello-
那这不是还有特征?

我们来抓个包看看,这是访问 www.xuexi.cn ,我们知道www.xuexi.cn 是没有启用ECH的


再来看看 konachan.com ,我们知道 konachan.com是启用ECH的

通过抓包分析,我们发现无论是否使用ECH,现在的浏览器构建的clientHello的encrypted_client_hello字段全都是0xfe0d,现在是无法根据这个特征来识别ECH流量的,因为ECH流量与非ECH流量混淆在一起

虽然无法通过encrypted_client_hello的版本字段来识别ECH,但还是可以通过流量分析来识别 https://blog.cloudflare.com/encrypted-client-hello/#resistance-to-traffic-analysis
加密的 ClientHello 的长度可能会泄露有关 SNI 的足够信息,使对手能够对其值做出有根据的猜测(对于特别短或特别长的域名,这种风险尤其高)。在没有更详细的攻击实例和研究论文发布之前,我暂时不对此进行深入讨论。

另外,如果Chrome系浏览器采用明文DNS查询来尝试访问支持ECH的网站,那么在第一次访问时仍会暴露SNI,这也可以视为一种实现上的漏洞,也许会成为一种特征,毕竟两次都会连同一个IP,不知道会不会修复

通过GFW的丢弃数据包的现象,也能猜测对于能识别的且在黑名单里的加密协议的数据包GFW采取直接丢弃的策略

自己编译支持ECH的nginx并创建支持ECH的网站
https://kabe.dev/nginx-ech-test/
openssl、nginx全都要自编译太麻烦,先挖坑,不一定会填

NekoBox 支持翻墙协议流量实现ECH
https://telegra.ph/Enable-ECH-for-sing-box-server-and-CatBoxNekoXray-client-10-06
先挖坑,不一定会填

[1] 冲浪小本本儿(一) :heavy_check_mark:
[2] 冲浪小本本儿(一)(1-1) byedpi/zapret/spoofdpi :heavy_check_mark:
[3]冲浪小本本儿(二)---- http-https-ECH 最广泛、最真实、最管用的互联网隐私保护[1]— 简单说两句http;查看http报文;HTTP的TCP握手;OSI模型下的HTTP :heavy_check_mark:
[4] 冲浪小本本儿(二)(2-1)---- http-https-ECH 最广泛、最真实、最管用的互联网隐私保护[2]— 简单说两句https;非常重要的自签证书;后量子加密 :heavy_check_mark:
[5] 冲浪小本本儿 (二)(2-2)----http-https-ECH 最广泛、最真实、最管用的互联网隐私保护[3]— ECH详谈 :heavy_check_mark:
[6]冲浪小本本儿 (二)(2-3)----伊友闹得欢,rprx拉清单,小圈子大新闻始末 :heavy_check_mark:
[7]冲浪小本本儿 (三) — 什么是desync类工具,还有哪些直连类型工具;什么是正代反代,如何完成反代;steam++这类工具是怎么完成不连接第三方服务器直连的;简易反代制作;当你直连一个网站时外部能看到什么
[8]冲浪小本本儿 (三)(3-1) 什么是tun模式,为什么clash的tun模式叫tun模式,而不叫tunnel
[9] 冲浪小本本儿 (四) — 当你通过proxy翻墙时,会在日志里、服务器上留下什么,通过代理连接有哪些风险
[10] 冲浪小本本儿 (五) — 被墙的网站有哪些分级,一些简单的dns污染名单,sni阻断名单,直连有哪些风险
[11] 冲浪小本本儿 (六) — 如何对网站方隐藏自身,什么是浏览器指纹,怎么改变;分析怎么逃过宏迪追杀
[12] 冲浪小本本儿 (七) — 如何处理dns污染;低延迟但被污染的dns、高延迟但正确的dns怎么选?域名分流;doh本身被封了怎么办 :heavy_check_mark:
[13] 冲浪小本本儿 (七)(7-1)— 测试doh/dot/doq 是否被墙方法 ;adguard home自签证书让局域网内浏览器用上doh
[14]冲浪小本本儿(七) (7-2) — 旧文新更,从“DNS 在代理环境中的应用”说起;Firefox 使用自带的代理设置时, "使用 SOCKS v5 时代理 DNS 查询"开还是不开?令人尴尬的代理解析选择
[15]冲浪小本本儿(七) (7-3) — 不知道什么时候会开始写、能用得上的ODOH