diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 810b00d6..c24317e6 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -445,8 +445,15 @@ func (c *TLSConfig) Build() (proto.Message, error) { } } + if c.ECHConfig != "" { + ECHConfig, err := base64.StdEncoding.DecodeString(c.ECHConfig) + if err != nil { + return nil, errors.New("invalid ECH Config", c.ECHConfig) + } + config.EchConfig = ECHConfig + } + config.MasterKeyLog = c.MasterKeyLog - config.EchConfig = c.ECHConfig config.Ech_DOHserver = c.ECHDOHServer return config, nil diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 62fff59d..bf2c1b73 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -220,7 +220,7 @@ type Config struct { // @Critical PinnedPeerCertificatePublicKeySha256 [][]byte `protobuf:"bytes,14,rep,name=pinned_peer_certificate_public_key_sha256,json=pinnedPeerCertificatePublicKeySha256,proto3" json:"pinned_peer_certificate_public_key_sha256,omitempty"` MasterKeyLog string `protobuf:"bytes,15,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"` - EchConfig string `protobuf:"bytes,16,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` + EchConfig []byte `protobuf:"bytes,16,opt,name=ech_config,json=echConfig,proto3" json:"ech_config,omitempty"` Ech_DOHserver string `protobuf:"bytes,17,opt,name=ech_DOHserver,json=echDOHserver,proto3" json:"ech_DOHserver,omitempty"` } @@ -362,11 +362,11 @@ func (x *Config) GetMasterKeyLog() string { return "" } -func (x *Config) GetEchConfig() string { +func (x *Config) GetEchConfig() []byte { if x != nil { return x.EchConfig } - return "" + return nil } func (x *Config) GetEch_DOHserver() string { @@ -456,7 +456,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{ 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x10, 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, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x63, 0x68, 0x44, 0x4f, 0x48, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 3defb222..326e945c 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -88,6 +88,6 @@ message Config { repeated bytes pinned_peer_certificate_public_key_sha256 = 14; string master_key_log = 15; - string ech_config = 16; + bytes ech_config = 16; string ech_DOHserver = 17; } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index a7985899..6d0d9d98 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -7,10 +7,8 @@ import ( "bytes" "context" "crypto/tls" - "encoding/base64" "io" "net/http" - "regexp" "sync" "time" @@ -25,19 +23,15 @@ func ApplyECH(c *Config, config *tls.Config) error { var err error if len(c.EchConfig) > 0 { - ECHConfig, err = base64.StdEncoding.DecodeString(c.EchConfig) - if err != nil { - return errors.New("invalid ECH config") - } + ECHConfig = c.EchConfig } else { // ECH config > DOH lookup - if c.ServerName == "" { + if config.ServerName == "" { return errors.New("Using DOH for ECH needs serverName") } - ECHRecord, err := QueryRecord(c.ServerName, c.Ech_DOHserver) + ECHConfig, err = QueryRecord(c.ServerName, c.Ech_DOHserver) if err != nil { return err } - ECHConfig, _ = base64.StdEncoding.DecodeString(ECHRecord) } config.EncryptedClientHelloConfigList = ECHConfig @@ -45,7 +39,7 @@ func ApplyECH(c *Config, config *tls.Config) error { } type record struct { - record string + record []byte expire time.Time } @@ -54,34 +48,40 @@ var ( mutex sync.RWMutex ) -func QueryRecord(domain string, server string) (string, error) { - rec, found := dnsCache[domain] - if found && rec.expire.After(time.Now()) { - return rec.record, nil - } - mutex.Lock() - defer mutex.Unlock() - errors.LogDebug(context.Background(), "Tring to query ECH config for domain: ", domain, " with ECH server: ", server) - record, ttl, err := dohQuery(server, domain) - if err != nil { - return "", err - } - // Use TTL for good, but many HTTPS records have TTL 60, too short - if ttl < 600 { - ttl = 600 - } - rec.record = record - rec.expire = time.Now().Add(time.Second * time.Duration(ttl)) - dnsCache[domain] = rec - return record, nil +func QueryRecord(domain string, server string) ([]byte, error) { + mutex.Lock() + rec, found := dnsCache[domain] + if found && rec.expire.After(time.Now()) { + mutex.Unlock() + return rec.record, nil + } + mutex.Unlock() + + errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server) + record, ttl, err := dohQuery(server, domain) + if err != nil { + return []byte{}, err + } + + if ttl < 600 { + ttl = 600 + } + + mutex.Lock() + defer mutex.Unlock() + rec.record = record + rec.expire = time.Now().Add(time.Second * time.Duration(ttl)) + dnsCache[domain] = rec + return record, nil } -func dohQuery(server string, domain string) (string, uint32, error) { +func dohQuery(server string, domain string) ([]byte, uint32, error) { m := new(dns.Msg) m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS) + m.Id = 0 msg, err := m.Pack() if err != nil { - return "", 0, err + return []byte{}, 0, err } tr := &http.Transport{ IdleConnTimeout: 90 * time.Second, @@ -104,33 +104,37 @@ func dohQuery(server string, domain string) (string, uint32, error) { } req, err := http.NewRequest("POST", server, bytes.NewReader(msg)) if err != nil { - return "", 0, err + return []byte{}, 0, err } req.Header.Set("Content-Type", "application/dns-message") resp, err := client.Do(req) if err != nil { - return "", 0, err + return []byte{}, 0, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { - return "", 0, err + return []byte{}, 0, err } if resp.StatusCode != http.StatusOK { - return "", 0, errors.New("query failed with response code:", resp.StatusCode) + return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode) } respMsg := new(dns.Msg) err = respMsg.Unpack(respBody) if err != nil { - return "", 0, err + return []byte{}, 0, err } if len(respMsg.Answer) > 0 { - re := regexp.MustCompile(`ech="([^"]+)"`) - match := re.FindStringSubmatch(respMsg.Answer[0].String()) - if match[1] != "" { - errors.LogDebug(context.Background(), "Get ECH config:", match[1], " TTL:", respMsg.Answer[0].Header().Ttl) - return match[1], respMsg.Answer[0].Header().Ttl, nil + for _, answer := range respMsg.Answer { + if https, ok := answer.(*dns.HTTPS); ok && https.Hdr.Name == dns.Fqdn(domain) { + for _, v := range https.Value { + if echConfig, ok := v.(*dns.SVCBECHConfig); ok { + errors.LogDebug(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl) + return echConfig.ECH, answer.Header().Ttl, nil + } + } + } } } - return "", 0, errors.New("no ech record found") + return []byte{}, 0, errors.New("no ech record found") }