Update On Fri Dec 12 19:42:15 CET 2025

This commit is contained in:
github-action[bot]
2025-12-12 19:42:16 +01:00
parent 128874672f
commit 0707cc3b24
122 changed files with 7905 additions and 2647 deletions

View 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
}

View 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)
}
}

View File

@@ -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) {

View File

@@ -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
})
}