使用 Go1.24 的 http.Client 实现 tls ech

ECH stands for Encrypted Client Hello.

官方文档里面好像没提到怎么用

流程大概是:

  1. 获取 ECH 密钥
  2. 把密钥填到 TLSClientConfig
  3. 启动!

获取 ECH 密钥

先从 doh/dot 获取加密用的密钥,这里可以使用 google dns 的网页端获取,在 data 里面有一段 ech=xxxxx 的数据,xxxxx 就是 base64 后的密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"Status": 0 /* NOERROR */,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question": [
{
"name": "page.ankikong.cn.",
"type": 65 /* HTTPS */
}
],
"Answer": [
{
"name": "page.ankikong.cn.",
"type": 65 /* HTTPS */,
"TTL": 300,
"data": "1 . alpn=h3,h2 ipv4hint=104.21.16.1,104.21.32.1,104.21.48.1,104.21.64.1,104.21.80.1,104.21.96.1,104.21.112.1 ech=AEX+DQBBGAAgACAlEaEI0pTpVNSv+sawIJE/bCF77zEI0ssxo4KdmLjyMgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3030::6815:1001,2606:4700:3030::6815:2001,2606:4700:3030::6815:3001,2606:4700:3030::6815:4001,2606:4700:3030::6815:5001,2606:4700:3030::6815:6001,2606:4700:3030::6815:7001"
}
],
"Comment": "Response from 2a06:98c1:50::ac40:233b."
}

http1/2

把密钥抠下来,贴到下面的代码就能运行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net/http"
)

func main() {
key, err := base64.StdEncoding.DecodeString("AEX+DQBBGAAgACAlEaEI0pTpVNSv+sawIJE/bCF77zEI0ssxo4KdmLjyMgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA=")
if err != nil {
panic("Failed to decode base64 key:" + err.Error())
}
cli := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
EncryptedClientHelloConfigList: key,
MinVersion: tls.VersionTLS13,
},
},
}
res, err := cli.Get("https://page.ankikong.cn/cdn-cgi/trace")
if err != nil {
panic("Failed to make GET request: " + err.Error())
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
panic("Unexpected status code: " + res.Status)
}
data, err := io.ReadAll(res.Body)
if err != nil {
panic("Failed to read response body: " + err.Error())
}
fmt.Println(string(data))
}

请求成功后,可以在回包看到 sni=encrypted,说明成功触发 ECH 了。总的回包大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fl=fdsfas
h=page.ankikong.cn
ip=1.2.3.4
ts=1752406779.922
visit_scheme=https
uag=Go-http-client/1.1
colo=HKG
sliver=005-tier1
http=http/1.1
loc=CN
tls=TLSv1.3
sni=encrypted
warp=off
gateway=off
rbi=off
kex=X25519MLKEM768

http3

同样的,quic-go 也只需要设置一下 EncryptedClientHelloConfigList,就可以实现 ECH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net/http"

"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
)

func main() {
key, err := base64.StdEncoding.DecodeString("AEX+DQBBVgAgACBMnzqL5tVKzIGF6gOkQX9SOzYjF8Zrf0wQB//vzFWhFwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA=")
if err != nil {
panic("Failed to decode base64 key:" + err.Error())
}
tr := &http3.Transport{
TLSClientConfig: &tls.Config{
EncryptedClientHelloConfigList: key,
},
QUICConfig: &quic.Config{},
}
defer tr.Close()
cli := http.Client{
Transport: tr,
}

res, err := cli.Get("https://page.ankikong.cn/cdn-cgi/trace")
if err != nil {
panic("Failed to make GET request: " + err.Error())
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
panic("Unexpected status code: " + res.Status)
}
data, err := io.ReadAll(res.Body)
if err != nil {
panic("Failed to read response body: " + err.Error())
}
fmt.Println(string(data))
}

回包如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fl=fdsfas
h=page.ankikong.cn
ip=1.2.3.4
ts=1752408699.763
visit_scheme=https
uag=quic-go HTTP/3
colo=SJC
sliver=none
http=http/3
loc=CN
tls=TLSv1.3
sni=encrypted
warp=off
gateway=off
rbi=off
kex=X25519