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"`
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
ECHConfig string `json:"echConfig"`
ECHDOHServer string `json:"echDohServer"`
ECHDNSServer string `json:"echDnsServer"`
EchKeySets string `json:"echKeySets"`
}
@ -500,7 +500,7 @@ func (c *TLSConfig) Build() (proto.Message, error) {
}
config.EchKeySets = EchPrivateKey
}
config.Ech_DOHserver = c.ECHDOHServer
config.Ech_DNSserver = c.ECHDNSServer
return config, nil
}

View File

@ -444,7 +444,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
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)
if err != nil {
errors.LogError(context.Background(), err)

View File

@ -218,7 +218,7 @@ type Config struct {
// @Critical
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"`
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"`
}
@ -371,9 +371,9 @@ func (x *Config) GetEchConfig() []byte {
return nil
}
func (x *Config) GetEch_DOHserver() string {
func (x *Config) GetEch_DNSserver() string {
if x != nil {
return x.Ech_DOHserver
return x.Ech_DNSserver
}
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,
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,
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,
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,
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,

View File

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

View File

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