You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by el...@apache.org on 2018/11/09 17:14:19 UTC
[trafficcontrol] 01/05: Add TO Go /api/1.4/cdns/dnssec/refresh
This is an automated email from the ASF dual-hosted git repository.
elsloo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
commit a79ff4d127d297b76f3fa88ef51abd198e1b7ff7
Author: Robert Butts <ro...@apache.org>
AuthorDate: Tue Oct 9 13:40:36 2018 -0600
Add TO Go /api/1.4/cdns/dnssec/refresh
---
traffic_ops/traffic_ops_golang/api/api.go | 13 +-
.../traffic_ops_golang/cdn/dnssecrefresh.go | 413 +++++++++++++++++++++
traffic_ops/traffic_ops_golang/routes.go | 2 +
3 files changed, 420 insertions(+), 8 deletions(-)
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index 26755a6..5143828 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -291,11 +291,11 @@ type APIInfo struct {
// }
//
func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) (*APIInfo, error, error, int) {
- db, err := getDB(r.Context())
+ db, err := GetDB(r.Context())
if err != nil {
return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting db: " + err.Error()), nil, http.StatusInternalServerError
}
- cfg, err := getConfig(r.Context())
+ cfg, err := GetConfig(r.Context())
if err != nil {
return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting config: " + err.Error()), nil, http.StatusInternalServerError
}
@@ -336,7 +336,8 @@ func (inf *APIInfo) Close() {
}
}
-func getDB(ctx context.Context) (*sqlx.DB, error) {
+// GetDB returns the database from the context. This should very rarely be needed, rather `NewInfo` should always be used to get a transaction, except in extenuating circumstances.
+func GetDB(ctx context.Context) (*sqlx.DB, error) {
val := ctx.Value(DBContextKey)
if val != nil {
switch v := val.(type) {
@@ -349,7 +350,7 @@ func getDB(ctx context.Context) (*sqlx.DB, error) {
return nil, errors.New("No db found in Context")
}
-func getConfig(ctx context.Context) (*config.Config, error) {
+func GetConfig(ctx context.Context) (*config.Config, error) {
val := ctx.Value(ConfigContextKey)
if val != nil {
switch v := val.(type) {
@@ -362,10 +363,6 @@ func getConfig(ctx context.Context) (*config.Config, error) {
return nil, errors.New("No config found in Context")
}
-func GetConfig(ctx context.Context) (*config.Config, error) {
- return getConfig(ctx)
-}
-
func getReqID(ctx context.Context) (uint64, error) {
val := ctx.Value(ReqIDContextKey)
if val != nil {
diff --git a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
new file mode 100644
index 0000000..0abdd1b
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
@@ -0,0 +1,413 @@
+package cdn
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ // "context"
+ "database/sql"
+ "errors"
+ "net/http"
+ "strconv"
+ "sync/atomic"
+ "time"
+
+ "github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "github.com/apache/trafficcontrol/lib/go-util"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
+
+ "github.com/lib/pq"
+)
+
+const RefreshDNSSECKeysTxTimeout = time.Duration(60) * time.Second // TODO: make configurable?
+
+func RefreshDNSSECKeys(w http.ResponseWriter, r *http.Request) {
+ if setInDNSSECKeyRefresh() {
+ db, err := api.GetDB(r.Context())
+ noTx := (*sql.Tx)(nil) // make a variable instead of passing nil directly, to reduce copy-paste errors
+ if err != nil {
+ api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys getting db from context: "+err.Error()))
+ return
+ }
+ cfg, err := api.GetConfig(r.Context())
+ if err != nil {
+ api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys getting config from context: "+err.Error()))
+ return
+ }
+
+ // dbCtx, _ := context.WithTimeout(r.Context(), RefreshDNSSECKeysTxTimeout)
+ // tx, err := db.BeginTx(dbCtx, nil)
+ // if err != nil {
+ // api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys beginning transaction: "+err.Error()))
+ // }
+
+ tx, err := db.Begin()
+ if err != nil {
+ api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys beginning tx: "+err.Error()))
+ }
+ go doDNSSECKeyRefresh(tx, cfg) // doDNSSECKeyRefresh takes ownership of tx and MUST close it.
+ } else {
+ log.Infoln("RefreshDNSSECKeys called, while server was concurrently executing a refresh, doing nothing")
+ }
+
+ api.WriteResp(w, r, "Checking DNSSEC keys for refresh in the background")
+}
+
+const DNSSECKeyRefreshDefaultTTL = time.Duration(60) * time.Second
+const DNSSECKeyRefreshDefaultGenerationMultiplier = uint64(10)
+const DNSSECKeyRefreshDefaultEffectiveMultiplier = uint64(10)
+const DNSSECKeyRefreshDefaultKSKExpiration = time.Duration(365) * time.Hour * 24
+const DNSSECKeyRefreshDefaultZSKExpiration = time.Duration(30) * time.Hour * 24
+
+// doDNSSECKeyRefresh refreshes the CDN's DNSSEC keys, as necessary.
+// This takes ownership of tx, and MUST call `tx.Close()`.
+// This SHOULD only be called if setInDNSSECKeyRefresh() returned true, in which case this MUST call unsetInDNSSECKeyRefresh() before returning.
+func doDNSSECKeyRefresh(tx *sql.Tx, cfg *config.Config) {
+ doCommit := true
+ defer func() {
+ if doCommit {
+ tx.Commit()
+ } else {
+ tx.Rollback()
+ }
+ }()
+ defer unsetInDNSSECKeyRefresh()
+
+ updatedAny := false
+
+ cdnDNSSECKeyParams, err := getDNSSECKeyRefreshParams(tx)
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: getting cdn parameters: " + err.Error())
+ doCommit = false
+ return
+ }
+ cdns := []string{}
+ for _, inf := range cdnDNSSECKeyParams {
+ if inf.DNSSECEnabled {
+ cdns = append(cdns, string(inf.CDNName))
+ }
+ }
+ // TODO change to return a slice, map is slow and unnecessary
+ dsInfo, err := getDNSSECKeyRefreshDSInfo(tx, cdns)
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: getting ds info: " + err.Error())
+ doCommit = false
+ return
+ }
+ dses := []string{}
+ for ds, _ := range dsInfo {
+ dses = append(dses, string(ds))
+ }
+
+ dsMatchlists, err := deliveryservice.GetDeliveryServicesMatchLists(dses, tx) // map[string][]tc.DeliveryServiceMatch
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: getting ds matchlists: " + err.Error())
+ doCommit = false
+ return
+ }
+ exampleURLs := map[tc.DeliveryServiceName][]string{}
+ for ds, inf := range dsInfo {
+ exampleURLs[ds] = deliveryservice.MakeExampleURLs(inf.Protocol, inf.Type, inf.RoutingName, dsMatchlists[string(ds)], inf.CDNDomain)
+ }
+
+ for _, cdnInf := range cdnDNSSECKeyParams {
+ keys, ok, err := riaksvc.GetDNSSECKeys(string(cdnInf.CDNName), tx, cfg.RiakAuthOptions) // TODO get all in a map beforehand
+ if err != nil {
+ log.Warnln("refreshing DNSSEC Keys: getting cdn '" + string(cdnInf.CDNName) + "' keys from Riak, skipping: " + err.Error())
+ continue
+ }
+ if !ok {
+ log.Warnln("refreshing DNSSEC Keys: cdn '" + string(cdnInf.CDNName) + "' has no keys in Riak, skipping")
+ continue
+ }
+
+ ttl := DNSSECKeyRefreshDefaultTTL
+ if cdnInf.TLDTTLsDNSKEY != nil {
+ ttl = time.Duration(*cdnInf.TLDTTLsDNSKEY) * time.Second
+ }
+
+ genMultiplier := DNSSECKeyRefreshDefaultGenerationMultiplier
+ if cdnInf.DNSKEYGenerationMultiplier != nil {
+ genMultiplier = *cdnInf.DNSKEYGenerationMultiplier
+ }
+
+ effectiveMultiplier := DNSSECKeyRefreshDefaultEffectiveMultiplier
+ if cdnInf.DNSKEYEffectiveMultiplier != nil {
+ effectiveMultiplier = *cdnInf.DNSKEYEffectiveMultiplier
+ }
+
+ nowPlusTTL := time.Now().Add(ttl * time.Duration(genMultiplier)) // "key_expiration" in the Perl this was transliterated from
+
+ defaultKSKExpiration := DNSSECKeyRefreshDefaultKSKExpiration
+ for _, key := range keys[string(cdnInf.CDNName)].KSK {
+ if key.Status != tc.DNSSECKeyStatusNew {
+ continue
+ }
+ defaultKSKExpiration = time.Unix(key.ExpirationDateUnix, 0).Sub(time.Unix(key.InceptionDateUnix, 0))
+ break
+ }
+
+ defaultZSKExpiration := DNSSECKeyRefreshDefaultZSKExpiration
+ for _, key := range keys[string(cdnInf.CDNName)].ZSK {
+ if key.Status != tc.DNSSECKeyStatusNew {
+ continue
+ }
+ expiration := time.Unix(key.ExpirationDateUnix, 0)
+ inception := time.Unix(key.InceptionDateUnix, 0)
+ defaultZSKExpiration = expiration.Sub(inception)
+
+ if expiration.After(nowPlusTTL) {
+ continue
+ }
+ log.Infoln("The ZSK keys for '" + string(cdnInf.CDNName) + "' are expired!")
+ effectiveDate := expiration.Add(ttl * time.Duration(effectiveMultiplier) * -1) // -1 to subtract
+ isKSK := false
+ newKeys, err := regenExpiredKeys(isKSK, keys[string(cdnInf.CDNName)], effectiveDate, false, false)
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: regenerating expired ZSK keys: " + err.Error())
+ } else {
+ keys[string(cdnInf.CDNName)] = newKeys
+ updatedAny = true
+ }
+ }
+
+ for _, ds := range dsInfo {
+ if ds.CDNName != cdnInf.CDNName {
+ continue
+ }
+ if t := ds.Type; !t.IsHTTP() && !t.IsSteering() && !t.IsDNS() {
+ continue
+ }
+
+ dsKeys, dsKeysExist := keys[string(ds.DSName)]
+ if !dsKeysExist {
+ log.Infoln("Keys do not exist for ds '" + string(ds.DSName) + "'")
+
+ cdnKeys, ok := keys[string(ds.CDNName)]
+ if !ok {
+ log.Errorln("refreshing DNSSEC Keys: cdn has no keys, cannot create ds keys: " + err.Error())
+ continue
+ }
+
+ overrideTTL := false
+ dsKeys, err := deliveryservice.CreateDNSSECKeys(tx, cfg, string(ds.DSName), exampleURLs[ds.DSName], cdnKeys, defaultKSKExpiration, defaultZSKExpiration, ttl, overrideTTL)
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: creating missing ds keys: " + err.Error())
+ }
+ keys[string(ds.DSName)] = dsKeys
+ updatedAny = true
+ continue
+ }
+
+ for _, key := range dsKeys.KSK {
+ if key.Status != tc.DNSSECKeyStatusNew {
+ continue
+ }
+ expiration := time.Unix(key.ExpirationDateUnix, 0)
+ if expiration.After(nowPlusTTL) {
+ continue
+ }
+ log.Infoln("The KSK keys for '" + ds.DSName + "' are expired!")
+ effectiveDate := expiration.Add(ttl * time.Duration(effectiveMultiplier) * -1) // -1 to subtract
+ isKSK := true
+ newKeys, err := regenExpiredKeys(isKSK, dsKeys, effectiveDate, false, false)
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: regenerating expired KSK keys for ds '" + string(ds.DSName) + "': " + err.Error())
+ } else {
+ keys[string(ds.DSName)] = newKeys
+ updatedAny = true
+ }
+ }
+
+ for _, key := range dsKeys.ZSK {
+ if key.Status != tc.DNSSECKeyStatusNew {
+ continue
+ }
+ expiration := time.Unix(key.ExpirationDateUnix, 0)
+ if expiration.After(nowPlusTTL) {
+ continue
+ }
+ log.Infoln("The ZSK keys for '" + ds.DSName + "' are expired!")
+ effectiveDate := expiration.Add(ttl * time.Duration(effectiveMultiplier) * -1) // -1 to add
+ isKSK := false
+ newKeys, err := regenExpiredKeys(isKSK, dsKeys, effectiveDate, false, false)
+ if err != nil {
+ log.Errorln("refreshing DNSSEC Keys: regenerating expired ZSK keys for ds '" + string(ds.DSName) + "': " + err.Error())
+ } else {
+ keys[string(ds.DSName)] = newKeys
+ updatedAny = true
+ }
+ }
+ }
+ if updatedAny {
+ if err := riaksvc.PutDNSSECKeys(keys, string(cdnInf.CDNName), tx, cfg.RiakAuthOptions); err != nil {
+ log.Errorln("refreshing DNSSEC Keys: putting keys into Riak for cdn '" + string(cdnInf.CDNName) + "': " + err.Error())
+ }
+ }
+ }
+ log.Infoln("Done refreshing DNSSEC keys")
+}
+
+type DNSSECKeyRefreshCDNInfo struct {
+ CDNName tc.CDNName
+ DNSSECEnabled bool
+ TLDTTLsDNSKEY *uint64
+ DNSKEYEffectiveMultiplier *uint64
+ DNSKEYGenerationMultiplier *uint64
+}
+
+// getDNSSECKeyRefreshParams returns returns the CDN's profile's tld.ttls.DNSKEY, DNSKEY.effective.multiplier, and DNSKEY.generation.multiplier parameters. If either parameter doesn't exist, nil is returned.
+// If a CDN exists, but has no parameters, it is returned as a key in the map with a nil value.
+func getDNSSECKeyRefreshParams(tx *sql.Tx) (map[tc.CDNName]DNSSECKeyRefreshCDNInfo, error) {
+ qry := `
+WITH cdn_profile_ids AS (
+ SELECT
+ DISTINCT(c.name) as cdn_name,
+ c.dnssec_enabled as cdn_dnssec_enabled,
+ MAX(p.id) as profile_id -- We only want 1 profile, so get the probably-newest if there's more than one.
+ FROM
+ cdn c
+ LEFT JOIN profile p ON c.id = p.cdn AND (p.name like 'CCR%' OR p.name like 'TR%')
+ GROUP BY c.name, c.dnssec_enabled
+)
+SELECT
+ DISTINCT(pi.cdn_name),
+ pi.cdn_dnssec_enabled,
+ MAX(pa.name) as parameter_name,
+ MAX(pa.value) as parameter_value
+FROM
+ cdn_profile_ids pi
+ LEFT JOIN profile pr ON pi.profile_id = pr.id
+ LEFT JOIN profile_parameter pp ON pr.id = pp.profile
+ LEFT JOIN parameter pa ON pp.parameter = pa.id AND (
+ pa.name = 'tld.ttls.DNSKEY'
+ OR pa.name = 'DNSKEY.effective.multiplier'
+ OR pa.name = 'DNSKEY.generation.multiplier'
+ )
+GROUP BY pi.cdn_name, pi.cdn_dnssec_enabled
+`
+ rows, err := tx.Query(qry)
+ if err != nil {
+ return nil, errors.New("getting cdn dnssec key refresh parameters: " + err.Error())
+ }
+ defer rows.Close()
+
+ params := map[tc.CDNName]DNSSECKeyRefreshCDNInfo{}
+ for rows.Next() {
+ cdnName := tc.CDNName("")
+ dnssecEnabled := false
+ name := util.StrPtr("")
+ valStr := util.StrPtr("")
+ if err := rows.Scan(&cdnName, &dnssecEnabled, &name, &valStr); err != nil {
+ return nil, errors.New("scanning cdn dnssec key refresh parameters: " + err.Error())
+ }
+
+ inf := params[cdnName]
+ inf.CDNName = cdnName
+ inf.DNSSECEnabled = dnssecEnabled
+
+ if name == nil || valStr == nil {
+ // no DNSKEY parameters, but the CDN still exists.
+ params[cdnName] = inf
+ continue
+ }
+
+ val, err := strconv.ParseUint(*valStr, 10, 64)
+ if err != nil {
+ log.Warnln("getting CDN dnssec refresh parameters: parameter '" + *name + "' value '" + *valStr + "' is not a number, skipping")
+ params[cdnName] = inf
+ continue
+ }
+
+ switch *name {
+ case "tld.ttls.DNSKEY":
+ inf.TLDTTLsDNSKEY = &val
+ case "DNSKEY.effective.multiplier":
+ inf.DNSKEYEffectiveMultiplier = &val
+ case "DNSKEY.generation.multiplier":
+ inf.DNSKEYGenerationMultiplier = &val
+ default:
+ log.Warnln("getDNSSECKeyRefreshParams got unknown parameter '" + *name + "', skipping")
+ continue
+ }
+ params[cdnName] = inf
+ }
+ return params, nil
+}
+
+type DNSSECKeyRefreshDSInfo struct {
+ DSName tc.DeliveryServiceName
+ Type tc.DSType
+ Protocol *int
+ CDNName tc.CDNName
+ CDNDomain string
+ RoutingName string
+}
+
+func getDNSSECKeyRefreshDSInfo(tx *sql.Tx, cdns []string) (map[tc.DeliveryServiceName]DNSSECKeyRefreshDSInfo, error) {
+ qry := `
+SELECT
+ ds.xml_id,
+ tp.name as type,
+ ds.protocol,
+ c.name as cdn_name,
+ c.domain_name as cdn_domain,
+ ds.routing_name
+FROM
+ deliveryservice ds
+ JOIN type tp ON tp.id = ds.type
+ JOIN cdn c ON c.id = ds.cdn_id
+WHERE
+ c.name = ANY($1)
+`
+ rows, err := tx.Query(qry, pq.Array(cdns))
+ if err != nil {
+ return nil, errors.New("getting cdn dnssec key refresh ds info: " + err.Error())
+ }
+ defer rows.Close()
+
+ dsInf := map[tc.DeliveryServiceName]DNSSECKeyRefreshDSInfo{}
+ for rows.Next() {
+ i := DNSSECKeyRefreshDSInfo{}
+ if err := rows.Scan(&i.DSName, &i.Type, &i.Protocol, &i.CDNName, &i.CDNDomain, &i.RoutingName); err != nil {
+ return nil, errors.New("scanning cdn dnssec key refresh ds info: " + err.Error())
+ }
+ dsInf[i.DSName] = i
+ }
+ return dsInf, nil
+}
+
+// inDNSSECKeyRefresh is whether the server is currently processing a refresh in the background.
+// This is used to only perform 1 refresh at a time.
+// This MUST NOT be changed outside of atomic operations.
+// This MUST NOT be changed to a boolean, or set without atomics. Atomic semantics involve more than just setting a memory location.
+var inDNSSECKeyRefresh = uint64(0)
+
+// setInDNSSECKeyRefresh attempts to set whether the server is currently executing a DNSSEC key refresh operation.
+// Returns false if a refresh operation is already executing.
+// If this returns true, the caller MUST call unsetInDNSSECKeyRefresh().
+func setInDNSSECKeyRefresh() bool { return atomic.CompareAndSwapUint64(&inDNSSECKeyRefresh, 0, 1) }
+
+// unsetInDNSSECKeyRefresh sets the flag indicating that the server is currently executing a DNSSEC key refresh operation to false.
+// This MUST NOT be called, unless setInDNSSECKeyRefresh() was previously called and returned true.
+func unsetInDNSSECKeyRefresh() { atomic.StoreUint64(&inDNSSECKeyRefresh, 0) }
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index f7d4715..9b41047 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -137,6 +137,8 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
{1.1, http.MethodPost, `cdns/dnsseckeys/generate(\.json)?$`, cdn.CreateDNSSECKeys, auth.PrivLevelAdmin, Authenticated, nil},
{1.1, http.MethodGet, `cdns/name/{name}/dnsseckeys/delete/?(\.json)?$`, cdn.DeleteDNSSECKeys, auth.PrivLevelAdmin, Authenticated, nil},
+ {1.4, http.MethodGet, `cdns/dnsseckeys/refresh/?(\.json)?$`, cdn.RefreshDNSSECKeys, auth.PrivLevelOperations, Authenticated, nil},
+
//CDN: Monitoring: Traffic Monitor
{1.1, http.MethodGet, `cdns/{cdn}/configs/monitoring(\.json)?$`, monitoring.Get, auth.PrivLevelReadOnly, Authenticated, nil},