From 6d5be8694736fd40c7e36752bf874bb5c376fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Sun, 9 Mar 2025 09:43:40 +0000 Subject: [PATCH] Add classic UDP DNS support for ECH Config --- infra/conf/transport_internet.go | 4 +- transport/internet/tls/config.go | 2 +- transport/internet/tls/config.pb.go | 10 +-- transport/internet/tls/config.proto | 2 +- transport/internet/tls/ech.go | 130 ++++++++++++++++++---------- 5 files changed, 91 insertions(+), 57 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 39dbe73d..07fe4aa3 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -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 } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 0193f931..7c86b406 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -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) diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index ef483451..e8d218a4 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -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, diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 09b93999..df3bcb05 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -94,7 +94,7 @@ message Config { bytes ech_config = 18; - string ech_DOHserver = 19; + string ech_DNSserver = 19; bytes ech_key_sets = 20; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 20156fde..cbe13042 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -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 {