diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 38a0b33f3..836fa1336 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -17,7 +17,6 @@ import ( "github.com/cloudflare/circl/hpke" "github.com/cloudflare/circl/kem" "github.com/libdns/libdns" - "github.com/zeebo/blake3" "go.uber.org/zap" "golang.org/x/crypto/cryptobyte" @@ -382,9 +381,12 @@ func (dnsPub ECHDNSPublisher) Provision(ctx caddy.Context) error { } func (dnsPub ECHDNSPublisher) PublisherKey() string { - h := blake3.New() - fmt.Fprintf(h, "%v", dnsPub.provider) - return dnsPub.provider.(caddy.Module).CaddyModule().ID.Name() + ":" + string(h.Sum(nil)) + // TODO: Figure out what the key should be + // h := blake3.New() + // fmt.Fprintf(h, "%v", dnsPub.provider) + // return fmt.Sprintf("%s::%s", dnsPub.provider.(caddy.Module).CaddyModule().ID, h.Sum(nil)) + return string(dnsPub.provider.(caddy.Module).CaddyModule().ID) + } func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, configListBin []byte) error { @@ -850,14 +852,34 @@ type ECHPublisher interface { // Publishes the ECH config list for the given innerNames. Some publishers // may not need a list of inner/protected names, and can ignore the argument; - // most, however, will want to use it as guidance to ensure the inner names - // are associated with the proper ECH configs. + // most, however, will want to use it to know which inner names are to be + // associated with the given ECH config list. PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error } type echConfigMeta struct { - Created time.Time `json:"created"` - Publications map[string]time.Time `json:"publications"` // map of publisher ID to timestamp of publication + Created time.Time `json:"created"` + Publications publicationHistory `json:"publications"` +} + +// publicationHistory is a map of publisher key to +// map of inner name to timestamp +type publicationHistory map[string]map[string]time.Time + +func (hist publicationHistory) unpublishedNames(publisherKey string, serverNamesSet map[string]struct{}) map[string]struct{} { + innerNamesSet, ok := hist[publisherKey] + if !ok { + // no history of this publisher publishing this config at all, so publish for entire set of names + return serverNamesSet + } + for innerName := range innerNamesSet { + // names in this loop have already had this config published by this publisher, + // so delete them from the set of names to publish for + // + // TODO: Potentially utilize the timestamp (map value) to preserve server name for re-publication if enough time has passed + delete(serverNamesSet, innerName) + } + return serverNamesSet } // The key prefix when putting ECH configs in storage. After this diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 0b7c56f38..7b9d5ae30 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -22,7 +22,9 @@ import ( "log" "net" "net/http" + "path" "runtime/debug" + "strconv" "strings" "sync" "time" @@ -430,25 +432,61 @@ func (t *TLS) Start() error { if err != nil { return fmt.Errorf("marshaling ECH config list: %v", err) } - var serverNames []string + + // by default, publish for all (non-outer) server names, unless + // a specific list of names is configured + var serverNamesSet map[string]struct{} + if publication.DNSNames == nil { + serverNamesSet = make(map[string]struct{}, len(t.serverNames)) + for name := range t.serverNames { + serverNamesSet[name] = struct{}{} + } + } else { + serverNamesSet = make(map[string]struct{}, len(publication.DNSNames)) + for _, name := range publication.DNSNames { + serverNamesSet[name] = struct{}{} + } + } + for _, publisher := range publication.publishers { - dnsNames := publication.DNSNames - if dnsNames == nil { - // by default, publish for all (non-outer) server names; convert - // de-duplicated map of server names to a slice - if serverNames == nil { - serverNames = make([]string, 0, len(t.serverNames)) - for sn := range t.serverNames { - serverNames = append(serverNames, sn) + publisherKey := publisher.PublisherKey() + for _, cfg := range echCfgList { + serverNamesSet = cfg.meta.Publications.unpublishedNames(publisherKey, serverNamesSet) + } + if len(serverNamesSet) > 0 { + dnsNamesToPublish := make([]string, 0, len(serverNamesSet)) + for name := range serverNamesSet { + dnsNamesToPublish = append(dnsNamesToPublish, name) + } + pubTime := time.Now() + err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) + if err != nil { + t.logger.Error("publishing ECH configuration list", + zap.Strings("for_dns_names", publication.DNSNames), + zap.Error(err)) + } + + // update publication history + for _, cfg := range echCfgList { + if cfg.meta.Publications == nil { + cfg.meta.Publications = make(publicationHistory) + } + if _, ok := cfg.meta.Publications[publisherKey]; !ok { + cfg.meta.Publications[publisherKey] = make(map[string]time.Time) + } + for _, name := range dnsNamesToPublish { + cfg.meta.Publications[publisherKey][name] = pubTime + } + metaBytes, err := json.Marshal(cfg.meta) + if err != nil { + return fmt.Errorf("marshaling ECH config metadata: %v", err) + } + parentKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID))) + metaKey := path.Join(parentKey, "meta.json") + if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil { + return fmt.Errorf("storing ECH config metadata: %v", err) } } - dnsNames = serverNames - } - err := publisher.PublishECHConfigList(t.ctx, dnsNames, echCfgListBin) - if err != nil { - t.logger.Error("publishing ECH configuration list", - zap.Strings("for_dns_names", publication.DNSNames), - zap.Error(err)) } } }