mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Fri Dec 12 19:42:15 CET 2025
This commit is contained in:
147
mihomo/component/ech/echparser/echparser.go
Normal file
147
mihomo/component/ech/echparser/echparser.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package echparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
// export from std's crypto/tls/ech.go
|
||||
|
||||
const extensionEncryptedClientHello = 0xfe0d
|
||||
|
||||
type ECHCipher struct {
|
||||
KDFID uint16
|
||||
AEADID uint16
|
||||
}
|
||||
|
||||
type ECHExtension struct {
|
||||
Type uint16
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type ECHConfig struct {
|
||||
raw []byte
|
||||
|
||||
Version uint16
|
||||
Length uint16
|
||||
|
||||
ConfigID uint8
|
||||
KemID uint16
|
||||
PublicKey []byte
|
||||
SymmetricCipherSuite []ECHCipher
|
||||
|
||||
MaxNameLength uint8
|
||||
PublicName []byte
|
||||
Extensions []ECHExtension
|
||||
}
|
||||
|
||||
var ErrMalformedECHConfigList = errors.New("tls: malformed ECHConfigList")
|
||||
|
||||
type EchConfigErr struct {
|
||||
field string
|
||||
}
|
||||
|
||||
func (e *EchConfigErr) Error() string {
|
||||
if e.field == "" {
|
||||
return "tls: malformed ECHConfig"
|
||||
}
|
||||
return fmt.Sprintf("tls: malformed ECHConfig, invalid %s field", e.field)
|
||||
}
|
||||
|
||||
func ParseECHConfig(enc []byte) (skip bool, ec ECHConfig, err error) {
|
||||
s := cryptobyte.String(enc)
|
||||
ec.raw = []byte(enc)
|
||||
if !s.ReadUint16(&ec.Version) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"version"}
|
||||
}
|
||||
if !s.ReadUint16(&ec.Length) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"length"}
|
||||
}
|
||||
if len(ec.raw) < int(ec.Length)+4 {
|
||||
return false, ECHConfig{}, &EchConfigErr{"length"}
|
||||
}
|
||||
ec.raw = ec.raw[:ec.Length+4]
|
||||
if ec.Version != extensionEncryptedClientHello {
|
||||
s.Skip(int(ec.Length))
|
||||
return true, ECHConfig{}, nil
|
||||
}
|
||||
if !s.ReadUint8(&ec.ConfigID) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"config_id"}
|
||||
}
|
||||
if !s.ReadUint16(&ec.KemID) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"kem_id"}
|
||||
}
|
||||
if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&ec.PublicKey)) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"public_key"}
|
||||
}
|
||||
var cipherSuites cryptobyte.String
|
||||
if !s.ReadUint16LengthPrefixed(&cipherSuites) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"cipher_suites"}
|
||||
}
|
||||
for !cipherSuites.Empty() {
|
||||
var c ECHCipher
|
||||
if !cipherSuites.ReadUint16(&c.KDFID) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"cipher_suites kdf_id"}
|
||||
}
|
||||
if !cipherSuites.ReadUint16(&c.AEADID) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"cipher_suites aead_id"}
|
||||
}
|
||||
ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c)
|
||||
}
|
||||
if !s.ReadUint8(&ec.MaxNameLength) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"maximum_name_length"}
|
||||
}
|
||||
var publicName cryptobyte.String
|
||||
if !s.ReadUint8LengthPrefixed(&publicName) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"public_name"}
|
||||
}
|
||||
ec.PublicName = publicName
|
||||
var extensions cryptobyte.String
|
||||
if !s.ReadUint16LengthPrefixed(&extensions) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"extensions"}
|
||||
}
|
||||
for !extensions.Empty() {
|
||||
var e ECHExtension
|
||||
if !extensions.ReadUint16(&e.Type) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"extensions type"}
|
||||
}
|
||||
if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) {
|
||||
return false, ECHConfig{}, &EchConfigErr{"extensions data"}
|
||||
}
|
||||
ec.Extensions = append(ec.Extensions, e)
|
||||
}
|
||||
|
||||
return false, ec, nil
|
||||
}
|
||||
|
||||
// ParseECHConfigList parses a draft-ietf-tls-esni-18 ECHConfigList, returning a
|
||||
// slice of parsed ECHConfigs, in the same order they were parsed, or an error
|
||||
// if the list is malformed.
|
||||
func ParseECHConfigList(data []byte) ([]ECHConfig, error) {
|
||||
s := cryptobyte.String(data)
|
||||
var length uint16
|
||||
if !s.ReadUint16(&length) {
|
||||
return nil, ErrMalformedECHConfigList
|
||||
}
|
||||
if length != uint16(len(data)-2) {
|
||||
return nil, ErrMalformedECHConfigList
|
||||
}
|
||||
var configs []ECHConfig
|
||||
for len(s) > 0 {
|
||||
if len(s) < 4 {
|
||||
return nil, errors.New("tls: malformed ECHConfig")
|
||||
}
|
||||
configLen := uint16(s[2])<<8 | uint16(s[3])
|
||||
skip, ec, err := ParseECHConfig(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = s[configLen+4:]
|
||||
if !skip {
|
||||
configs = append(configs, ec)
|
||||
}
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
30
mihomo/component/ech/key_test.go
Normal file
30
mihomo/component/ech/key_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package ech
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ech/echparser"
|
||||
)
|
||||
|
||||
func TestGenECHConfig(t *testing.T) {
|
||||
domain := "www.example.com"
|
||||
configBase64, _, err := GenECHConfig(domain)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
echConfigList, err := base64.StdEncoding.DecodeString(configBase64)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
echConfigs, err := echparser.ParseECHConfigList(echConfigList)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(echConfigs) == 0 {
|
||||
t.Error("no ech config")
|
||||
}
|
||||
if publicName := string(echConfigs[0].PublicName); publicName != domain {
|
||||
t.Error("ech config domain error, expect ", domain, " got", publicName)
|
||||
}
|
||||
}
|
||||
@@ -165,11 +165,9 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e
|
||||
|
||||
q := m.Question[0]
|
||||
domain := msgToDomain(m)
|
||||
_, qTypeStr := msgToQtype(m)
|
||||
cacheM, expireTime, hit := r.cache.GetWithExpire(q.String())
|
||||
if hit {
|
||||
ips := msgToIP(cacheM)
|
||||
log.Debugln("[DNS] cache hit %s --> %s %s, expire at %s", domain, ips, qTypeStr, expireTime.Format("2006-01-02 15:04:05"))
|
||||
log.Debugln("[DNS] cache hit %s --> %s, expire at %s", domain, msgToLogString(cacheM), expireTime.Format("2006-01-02 15:04:05"))
|
||||
now := time.Now()
|
||||
msg = cacheM.Copy()
|
||||
if expireTime.Before(now) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/picker"
|
||||
"github.com/metacubex/mihomo/component/ech/echparser"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
@@ -226,6 +227,81 @@ func msgToQtype(msg *D.Msg) (uint16, string) {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
func msgToHTTPSRRInfo(msg *D.Msg) string {
|
||||
var alpns []string
|
||||
var publicName string
|
||||
var hasIPv4, hasIPv6 bool
|
||||
|
||||
collect := func(rrs []D.RR) {
|
||||
for _, rr := range rrs {
|
||||
httpsRR, ok := rr.(*D.HTTPS)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, kv := range httpsRR.Value {
|
||||
switch v := kv.(type) {
|
||||
case *D.SVCBAlpn:
|
||||
if len(alpns) == 0 && len(v.Alpn) > 0 {
|
||||
alpns = append(alpns, v.Alpn...)
|
||||
}
|
||||
case *D.SVCBIPv4Hint:
|
||||
if len(v.Hint) > 0 {
|
||||
hasIPv4 = true
|
||||
}
|
||||
case *D.SVCBIPv6Hint:
|
||||
if len(v.Hint) > 0 {
|
||||
hasIPv6 = true
|
||||
}
|
||||
case *D.SVCBECHConfig:
|
||||
if publicName == "" && len(v.ECH) > 0 {
|
||||
if cfgs, err := echparser.ParseECHConfigList(v.ECH); err == nil && len(cfgs) > 0 {
|
||||
publicName = string(cfgs[0].PublicName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collect(msg.Answer)
|
||||
|
||||
//TODO: Do we need to process the data in msg.Extra?
|
||||
// If so, do we need to validate whether the domain names within it match our request?
|
||||
// To simplify the problem, let's ignore it for now.
|
||||
//collect(msg.Extra)
|
||||
|
||||
if len(alpns) == 0 && publicName == "" && !hasIPv4 && !hasIPv6 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
if len(alpns) > 0 {
|
||||
parts = append(parts, "alpn:"+strings.Join(alpns, ","))
|
||||
}
|
||||
if publicName != "" {
|
||||
parts = append(parts, "pn:"+publicName)
|
||||
}
|
||||
if hasIPv4 {
|
||||
parts = append(parts, "ipv4hint")
|
||||
}
|
||||
if hasIPv6 {
|
||||
parts = append(parts, "ipv6hint")
|
||||
}
|
||||
|
||||
return strings.Join(parts, ";")
|
||||
}
|
||||
|
||||
func msgToLogString(msg *D.Msg) string {
|
||||
qType, qTypeStr := msgToQtype(msg)
|
||||
switch qType {
|
||||
case D.TypeHTTPS:
|
||||
return fmt.Sprintf("[%s] %s", msgToHTTPSRRInfo(msg), qTypeStr)
|
||||
default:
|
||||
return fmt.Sprintf("%s %s", msgToIP(msg), qTypeStr)
|
||||
}
|
||||
}
|
||||
|
||||
func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, cache bool, err error) {
|
||||
cache = true
|
||||
fast, ctx := picker.WithTimeout[*D.Msg](ctx, resolver.DefaultDNSTimeout)
|
||||
@@ -248,8 +324,7 @@ func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.M
|
||||
// so we would ignore RCode errors from RCode clients.
|
||||
return nil, errors.New("server failure: " + D.RcodeToString[m.Rcode])
|
||||
}
|
||||
ips := msgToIP(m)
|
||||
log.Debugln("[DNS] %s --> %s %s from %s", domain, ips, qTypeStr, client.Address())
|
||||
log.Debugln("[DNS] %s --> %s from %s", domain, msgToLogString(m), client.Address())
|
||||
return m, nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user