Add classic UDP DNS support for ECH Config

pull/3813/head
风扇滑翔翼 2025-03-09 09:43:40 +00:00 committed by GitHub
parent 0923f53b21
commit 6d5be86947
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 91 additions and 57 deletions

View File

@ -413,7 +413,7 @@ type TLSConfig struct {
ServerNameToVerify string `json:"serverNameToVerify"` ServerNameToVerify string `json:"serverNameToVerify"`
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"` VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
ECHConfig string `json:"echConfig"` ECHConfig string `json:"echConfig"`
ECHDOHServer string `json:"echDohServer"` ECHDNSServer string `json:"echDnsServer"`
EchKeySets string `json:"echKeySets"` EchKeySets string `json:"echKeySets"`
} }
@ -500,7 +500,7 @@ func (c *TLSConfig) Build() (proto.Message, error) {
} }
config.EchKeySets = EchPrivateKey config.EchKeySets = EchPrivateKey
} }
config.Ech_DOHserver = c.ECHDOHServer config.Ech_DNSserver = c.ECHDNSServer
return config, nil return config, nil
} }

View File

@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
config.KeyLogWriter = writer config.KeyLogWriter = writer
} }
} }
if len(c.EchConfig) > 0 || len(c.Ech_DOHserver) > 0 || len(c.EchKeySets) > 0 { if len(c.EchConfig) > 0 || len(c.Ech_DNSserver) > 0 || len(c.EchKeySets) > 0 {
err := ApplyECH(c, config) err := ApplyECH(c, config)
if err != nil { if err != nil {
errors.LogError(context.Background(), err) errors.LogError(context.Background(), err)

View File

@ -218,7 +218,7 @@ type Config struct {
// @Critical // @Critical
VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"` VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"`
EchConfig []byte `protobuf:"bytes,18,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` EchConfig []byte `protobuf:"bytes,18,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"`
Ech_DOHserver string `protobuf:"bytes,19,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"` Ech_DNSserver string `protobuf:"bytes,19,opt,name=ech_DNSserver,json=echDNSserver,proto3" json:"ech_DNSserver,omitempty"`
EchKeySets []byte `protobuf:"bytes,20,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"` EchKeySets []byte `protobuf:"bytes,20,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"`
} }
@ -371,9 +371,9 @@ func (x *Config) GetEchConfig() []byte {
return nil return nil
} }
func (x *Config) GetEch_DOHserver() string { func (x *Config) GetEch_DNSserver() string {
if x != nil { if x != nil {
return x.Ech_DOHserver return x.Ech_DNSserver
} }
return "" return ""
} }
@ -468,9 +468,9 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x66, 0x69, 0x67, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4f, 0x48, 0x73, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x63, 0x68, 0x5f, 0x44, 0x4e, 0x53, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68,
0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x63, 0x68, 0x44, 0x4e, 0x53, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x63, 0x68,
0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63,
0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72,

View File

@ -94,7 +94,7 @@ message Config {
bytes ech_config = 18; bytes ech_config = 18;
string ech_DOHserver = 19; string ech_DNSserver = 19;
bytes ech_key_sets = 20; bytes ech_key_sets = 20;
} }

View File

@ -25,8 +25,8 @@ func ApplyECH(c *Config, config *tls.Config) error {
nameToQuery := c.ServerName nameToQuery := c.ServerName
var DOHServer string var DOHServer string
if len(c.EchConfig) != 0 || len(c.Ech_DOHserver) != 0 { if len(c.EchConfig) != 0 || len(c.Ech_DNSserver) != 0 {
parts := strings.Split(c.Ech_DOHserver, "+") parts := strings.Split(c.Ech_DNSserver, "+")
if len(parts) == 2 { if len(parts) == 2 {
// parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query" // parse ECH DOH server in format of "example.com+https://1.1.1.1/dns-query"
nameToQuery = parts[0] nameToQuery = parts[0]
@ -35,7 +35,7 @@ func ApplyECH(c *Config, config *tls.Config) error {
// normal format // normal format
DOHServer = parts[0] DOHServer = parts[0]
} else { } else {
return errors.New("Invalid ECH DOH server format: ", c.Ech_DOHserver) return errors.New("Invalid ECH DOH server format: ", c.Ech_DNSserver)
} }
if len(c.EchConfig) > 0 { if len(c.EchConfig) > 0 {
@ -133,55 +133,89 @@ func QueryRecord(domain string, server string) ([]byte, error) {
// return ECH config, TTL and error // return ECH config, TTL and error
func dohQuery(server string, domain string) ([]byte, uint32, error) { func dohQuery(server string, domain string) ([]byte, uint32, error) {
m := new(dns.Msg) m := new(dns.Msg)
var dnsResolve []byte
m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS)
// always 0 in DOH // for DOH server
m.Id = 0 if strings.HasPrefix(server, "https://") {
msg, err := m.Pack() // always 0 in DOH
if err != nil { m.Id = 0
return []byte{}, 0, err msg, err := m.Pack()
} if err != nil {
// All traffic sent by core should via xray's internet.DialSystem return []byte{}, 0, err
// This involves the behavior of some Android VPN GUI clients }
tr := &http.Transport{ // All traffic sent by core should via xray's internet.DialSystem
IdleConnTimeout: 90 * time.Second, // This involves the behavior of some Android VPN GUI clients
ForceAttemptHTTP2: true, tr := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { IdleConnTimeout: 90 * time.Second,
dest, err := net.ParseDestination(network + ":" + addr) ForceAttemptHTTP2: true,
if err != nil { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return nil, err dest, err := net.ParseDestination(network + ":" + addr)
} if err != nil {
conn, err := internet.DialSystem(ctx, dest, nil) return nil, err
if err != nil { }
return nil, err conn, err := internet.DialSystem(ctx, dest, nil)
} if err != nil {
return conn, nil return nil, err
}, }
} return conn, nil
client := &http.Client{ },
Timeout: 5 * time.Second, }
Transport: tr, client := &http.Client{
} Timeout: 5 * time.Second,
req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) Transport: tr,
if err != nil { }
return []byte{}, 0, err req, err := http.NewRequest("POST", server, bytes.NewReader(msg))
} if err != nil {
req.Header.Set("Content-Type", "application/dns-message") return []byte{}, 0, err
resp, err := client.Do(req) }
if err != nil { req.Header.Set("Content-Type", "application/dns-message")
return []byte{}, 0, err resp, err := client.Do(req)
} if err != nil {
defer resp.Body.Close() return []byte{}, 0, err
respBody, err := io.ReadAll(resp.Body) }
if err != nil { defer resp.Body.Close()
return []byte{}, 0, err respBody, err := io.ReadAll(resp.Body)
} if err != nil {
if resp.StatusCode != http.StatusOK { return []byte{}, 0, err
return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode) }
if resp.StatusCode != http.StatusOK {
return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode)
}
dnsResolve = respBody
} else if strings.HasPrefix(server, "udp://") { // for classic udp dns server
udpServerAddr := server[len("udp://"):]
// default port 53 if not specified
if !strings.Contains(udpServerAddr, ":") {
udpServerAddr = udpServerAddr + ":53"
}
dest, err := net.ParseDestination("udp" + ":" + udpServerAddr)
if err != nil {
return nil, 0, errors.New("failed to parse udp dns server ", udpServerAddr, " for ECH: ", err)
}
dnsTimeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// use xray's internet.DialSystem as mentioned above
conn, err := internet.DialSystem(dnsTimeoutCtx, dest, nil)
defer conn.Close()
if err != nil {
return []byte{}, 0, err
}
msg, err := m.Pack()
if err != nil {
return []byte{}, 0, err
}
conn.Write(msg)
udpResponse := make([]byte, 512)
_, err = conn.Read(udpResponse)
if err != nil {
return []byte{}, 0, err
}
dnsResolve = udpResponse
} }
respMsg := new(dns.Msg) respMsg := new(dns.Msg)
err = respMsg.Unpack(respBody) err := respMsg.Unpack(dnsResolve)
if err != nil { if err != nil {
return []byte{}, 0, err return []byte{}, 0, errors.New("failed to unpack dns response for ECH: ", err)
} }
if len(respMsg.Answer) > 0 { if len(respMsg.Answer) > 0 {
for _, answer := range respMsg.Answer { for _, answer := range respMsg.Answer {