Skip to content

Commit a1350d4

Browse files
committed
feat: add ech-key for listeners
1 parent dc958e6 commit a1350d4

38 files changed

+637
-44
lines changed

component/ech/key.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package ech
2+
3+
import (
4+
"crypto/ecdh"
5+
"crypto/rand"
6+
"encoding/base64"
7+
"encoding/pem"
8+
"errors"
9+
"fmt"
10+
"os"
11+
12+
"github.com/metacubex/mihomo/component/ca"
13+
tlsC "github.com/metacubex/mihomo/component/tls"
14+
15+
"golang.org/x/crypto/cryptobyte"
16+
)
17+
18+
const (
19+
AEAD_AES_128_GCM = 0x0001
20+
AEAD_AES_256_GCM = 0x0002
21+
AEAD_ChaCha20Poly1305 = 0x0003
22+
)
23+
24+
const extensionEncryptedClientHello = 0xfe0d
25+
const DHKEM_X25519_HKDF_SHA256 = 0x0020
26+
const KDF_HKDF_SHA256 = 0x0001
27+
28+
// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS.
29+
// We need this so that when we insert them into ECHConfigs the ordering
30+
// is stable.
31+
var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305}
32+
33+
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
34+
builder := cryptobyte.NewBuilder(nil)
35+
36+
builder.AddUint16(extensionEncryptedClientHello)
37+
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
38+
builder.AddUint8(id)
39+
40+
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
41+
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
42+
builder.AddBytes(pubKey)
43+
})
44+
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
45+
for _, aeadID := range sortedSupportedAEADs {
46+
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
47+
builder.AddUint16(aeadID)
48+
}
49+
})
50+
builder.AddUint8(maxNameLen)
51+
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
52+
builder.AddBytes([]byte(publicName))
53+
})
54+
builder.AddUint16(0) // extensions
55+
})
56+
57+
return builder.BytesOrPanic()
58+
}
59+
60+
func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) {
61+
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
62+
if err != nil {
63+
return
64+
}
65+
66+
echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
67+
68+
builder := cryptobyte.NewBuilder(nil)
69+
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
70+
builder.AddBytes(echConfig)
71+
})
72+
echConfigList := builder.BytesOrPanic()
73+
74+
builder2 := cryptobyte.NewBuilder(nil)
75+
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
76+
builder.AddBytes(echKey.Bytes())
77+
})
78+
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
79+
builder.AddBytes(echConfig)
80+
})
81+
echConfigKeys := builder2.BytesOrPanic()
82+
83+
configBase64 = base64.StdEncoding.EncodeToString(echConfigList)
84+
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys}))
85+
return
86+
}
87+
88+
func UnmarshalECHKeys(raw []byte) ([]tlsC.EncryptedClientHelloKey, error) {
89+
var keys []tlsC.EncryptedClientHelloKey
90+
rawString := cryptobyte.String(raw)
91+
for !rawString.Empty() {
92+
var key tlsC.EncryptedClientHelloKey
93+
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
94+
return nil, errors.New("error parsing private key")
95+
}
96+
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
97+
return nil, errors.New("error parsing config")
98+
}
99+
keys = append(keys, key)
100+
}
101+
if len(keys) == 0 {
102+
return nil, errors.New("empty ECH keys")
103+
}
104+
return keys, nil
105+
}
106+
107+
func LoadECHKey(key string, tlsConfig *tlsC.Config, path ca.Path) error {
108+
if key == "" {
109+
return nil
110+
}
111+
painTextErr := loadECHKey([]byte(key), tlsConfig)
112+
if painTextErr == nil {
113+
return nil
114+
}
115+
key = path.Resolve(key)
116+
var loadErr error
117+
if !path.IsSafePath(key) {
118+
loadErr = path.ErrNotSafePath(key)
119+
} else {
120+
var echKey []byte
121+
echKey, loadErr = os.ReadFile(key)
122+
if loadErr == nil {
123+
loadErr = loadECHKey(echKey, tlsConfig)
124+
}
125+
}
126+
if loadErr != nil {
127+
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
128+
}
129+
return nil
130+
}
131+
132+
func loadECHKey(echKey []byte, tlsConfig *tlsC.Config) error {
133+
block, rest := pem.Decode(echKey)
134+
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
135+
return errors.New("invalid ECH keys pem")
136+
}
137+
echKeys, err := UnmarshalECHKeys(block.Bytes)
138+
if err != nil {
139+
return fmt.Errorf("parse ECH keys: %w", err)
140+
}
141+
tlsConfig.EncryptedClientHelloKeys = echKeys
142+
return nil
143+
}

component/generater/cmd.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"encoding/base64"
55
"fmt"
66

7+
"github.com/metacubex/mihomo/component/ech"
8+
79
"github.com/gofrs/uuid/v5"
810
)
911

1012
func Main(args []string) {
1113
if len(args) < 1 {
12-
panic("Using: generate uuid/reality-keypair/wg-keypair")
14+
panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair")
1315
}
1416
switch args[0] {
1517
case "uuid":
@@ -33,5 +35,15 @@ func Main(args []string) {
3335
}
3436
fmt.Println("PrivateKey: " + privateKey.String())
3537
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
38+
case "ech-keypair":
39+
if len(args) < 2 {
40+
panic("Using: generate ech-keypair <plain_server_name>")
41+
}
42+
configBase64, keyPem, err := ech.GenECHConfig(args[1])
43+
if err != nil {
44+
panic(err)
45+
}
46+
fmt.Println("Config:", configBase64)
47+
fmt.Println("Key:", keyPem)
3648
}
3749
}

component/tls/utls.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn
2626
return utls.UClient(c, config, fingerprint)
2727
}
2828

29+
func NewListener(inner net.Listener, config *Config) net.Listener {
30+
return utls.NewListener(inner, config)
31+
}
32+
2933
func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) {
3034
if len(clientFingerprint) == 0 {
3135
clientFingerprint = globalFingerprint
@@ -93,7 +97,9 @@ func init() {
9397
fingerprints["randomized"] = randomized
9498
}
9599

96-
func UCertificates(it tls.Certificate) utls.Certificate {
100+
type Certificate = utls.Certificate
101+
102+
func UCertificate(it tls.Certificate) utls.Certificate {
97103
return utls.Certificate{
98104
Certificate: it.Certificate,
99105
PrivateKey: it.PrivateKey,
@@ -106,13 +112,15 @@ func UCertificates(it tls.Certificate) utls.Certificate {
106112
}
107113
}
108114

115+
type EncryptedClientHelloKey = utls.EncryptedClientHelloKey
116+
109117
type Config = utls.Config
110118

111119
func UConfig(config *tls.Config) *utls.Config {
112120
return &utls.Config{
113121
Rand: config.Rand,
114122
Time: config.Time,
115-
Certificates: utils.Map(config.Certificates, UCertificates),
123+
Certificates: utils.Map(config.Certificates, UCertificate),
116124
VerifyPeerCertificate: config.VerifyPeerCertificate,
117125
RootCAs: config.RootCAs,
118126
NextProtos: config.NextProtos,

docs/config.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,13 @@ listeners:
11551155
# 下面两项如果填写则开启 tls(需要同时填写)
11561156
# certificate: ./server.crt
11571157
# private-key: ./server.key
1158+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1159+
# ech-key: |
1160+
# -----BEGIN ECH KEYS-----
1161+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1162+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1163+
# dC5jb20AAA==
1164+
# -----END ECH KEYS-----
11581165

11591166
- name: http-in-1
11601167
type: http
@@ -1168,6 +1175,13 @@ listeners:
11681175
# 下面两项如果填写则开启 tls(需要同时填写)
11691176
# certificate: ./server.crt
11701177
# private-key: ./server.key
1178+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1179+
# ech-key: |
1180+
# -----BEGIN ECH KEYS-----
1181+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1182+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1183+
# dC5jb20AAA==
1184+
# -----END ECH KEYS-----
11711185

11721186
- name: mixed-in-1
11731187
type: mixed # HTTP(S) 和 SOCKS 代理混合
@@ -1182,6 +1196,13 @@ listeners:
11821196
# 下面两项如果填写则开启 tls(需要同时填写)
11831197
# certificate: ./server.crt
11841198
# private-key: ./server.key
1199+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1200+
# ech-key: |
1201+
# -----BEGIN ECH KEYS-----
1202+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1203+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1204+
# dC5jb20AAA==
1205+
# -----END ECH KEYS-----
11851206

11861207
- name: reidr-in-1
11871208
type: redir
@@ -1231,6 +1252,13 @@ listeners:
12311252
# 下面两项如果填写则开启 tls(需要同时填写)
12321253
# certificate: ./server.crt
12331254
# private-key: ./server.key
1255+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1256+
# ech-key: |
1257+
# -----BEGIN ECH KEYS-----
1258+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1259+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1260+
# dC5jb20AAA==
1261+
# -----END ECH KEYS-----
12341262
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
12351263
# reality-config:
12361264
# dest: test.com:443
@@ -1253,6 +1281,13 @@ listeners:
12531281
# 00000000-0000-0000-0000-000000000001: PASSWORD_1
12541282
# certificate: ./server.crt
12551283
# private-key: ./server.key
1284+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1285+
# ech-key: |
1286+
# -----BEGIN ECH KEYS-----
1287+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1288+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1289+
# dC5jb20AAA==
1290+
# -----END ECH KEYS-----
12561291
# congestion-controller: bbr
12571292
# max-idle-time: 15000
12581293
# authentication-timeout: 1000
@@ -1284,6 +1319,13 @@ listeners:
12841319
# 下面两项如果填写则开启 tls(需要同时填写)
12851320
# certificate: ./server.crt
12861321
# private-key: ./server.key
1322+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1323+
# ech-key: |
1324+
# -----BEGIN ECH KEYS-----
1325+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1326+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1327+
# dC5jb20AAA==
1328+
# -----END ECH KEYS-----
12871329
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
12881330
reality-config:
12891331
dest: test.com:443
@@ -1304,6 +1346,13 @@ listeners:
13041346
# "certificate" and "private-key" are required
13051347
certificate: ./server.crt
13061348
private-key: ./server.key
1349+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1350+
# ech-key: |
1351+
# -----BEGIN ECH KEYS-----
1352+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1353+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1354+
# dC5jb20AAA==
1355+
# -----END ECH KEYS-----
13071356
# padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme
13081357

13091358
- name: trojan-in-1
@@ -1320,6 +1369,13 @@ listeners:
13201369
# 下面两项如果填写则开启 tls(需要同时填写)
13211370
certificate: ./server.crt
13221371
private-key: ./server.key
1372+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1373+
# ech-key: |
1374+
# -----BEGIN ECH KEYS-----
1375+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1376+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1377+
# dC5jb20AAA==
1378+
# -----END ECH KEYS-----
13231379
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
13241380
# reality-config:
13251381
# dest: test.com:443
@@ -1345,6 +1401,13 @@ listeners:
13451401
00000000-0000-0000-0000-000000000001: PASSWORD_1
13461402
# certificate: ./server.crt
13471403
# private-key: ./server.key
1404+
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
1405+
# ech-key: |
1406+
# -----BEGIN ECH KEYS-----
1407+
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
1408+
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
1409+
# dC5jb20AAA==
1410+
# -----END ECH KEYS-----
13481411
## up 和 down 均不写或为 0 则使用 BBR 流控
13491412
# up: "30 Mbps" # 若不写单位,默认为 Mbps
13501413
# down: "200 Mbps" # 若不写单位,默认为 Mbps

listener/anytls/server.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package anytls
33
import (
44
"context"
55
"crypto/sha256"
6-
"crypto/tls"
76
"encoding/binary"
87
"errors"
98
"net"
@@ -13,6 +12,8 @@ import (
1312
"github.com/metacubex/mihomo/common/atomic"
1413
"github.com/metacubex/mihomo/common/buf"
1514
"github.com/metacubex/mihomo/component/ca"
15+
"github.com/metacubex/mihomo/component/ech"
16+
tlsC "github.com/metacubex/mihomo/component/tls"
1617
C "github.com/metacubex/mihomo/constant"
1718
LC "github.com/metacubex/mihomo/listener/config"
1819
"github.com/metacubex/mihomo/listener/sing"
@@ -28,7 +29,7 @@ type Listener struct {
2829
closed bool
2930
config LC.AnyTLSServer
3031
listeners []net.Listener
31-
tlsConfig *tls.Config
32+
tlsConfig *tlsC.Config
3233
userMap map[[32]byte]string
3334
padding atomic.TypedValue[*padding.PaddingFactory]
3435
}
@@ -41,13 +42,20 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
4142
}
4243
}
4344

44-
tlsConfig := &tls.Config{}
45+
tlsConfig := &tlsC.Config{}
4546
if config.Certificate != "" && config.PrivateKey != "" {
4647
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
4748
if err != nil {
4849
return nil, err
4950
}
50-
tlsConfig.Certificates = []tls.Certificate{cert}
51+
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
52+
53+
if config.EchKey != "" {
54+
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
55+
if err != nil {
56+
return nil, err
57+
}
58+
}
5159
}
5260

5361
sl = &Listener{
@@ -87,7 +95,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
8795
return nil, err
8896
}
8997
if len(tlsConfig.Certificates) > 0 {
90-
l = tls.NewListener(l, tlsConfig)
98+
l = tlsC.NewListener(l, tlsConfig)
9199
} else {
92100
return nil, errors.New("disallow using AnyTLS without certificates config")
93101
}

0 commit comments

Comments
 (0)