blob: d74c028897fcbaaaa53d4e22b1122d9d8b7fcec0 [file] [log] [blame]
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"
"fmt"
"math"
"net/http"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
)
const (
CDNDNSSECKeyType = "dnssec"
DNSSECStatusExisting = "existing"
DNSSECGenerationCPURatio = 0.66
)
func CreateDNSSECKeys(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
if !inf.Config.TrafficVaultEnabled {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys from Traffic Vault: Traffic Vault is not configured"))
return
}
req := tc.CDNDNSSECGenerateReq{}
if err := api.Parse(r.Body, inf.Tx.Tx, &req); err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("parsing request: "+err.Error()), nil)
return
}
if req.EffectiveDateUnix == nil {
now := tc.CDNDNSSECGenerateReqDate(time.Now().Unix())
req.EffectiveDateUnix = &now
}
cdnName := *req.Key
cdnID, ok, err := getCDNIDFromName(inf.Tx.Tx, tc.CDNName(cdnName))
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cdn ID from name '"+cdnName+"': "+err.Error()))
return
} else if !ok {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil)
return
}
cdnDomain, cdnExists, err := dbhelpers.GetCDNDomainFromName(inf.Tx.Tx, tc.CDNName(cdnName))
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("create DNSSEC keys: getting CDN domain: "+err.Error()))
return
} else if !cdnExists {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("cdn '"+cdnName+"' not found"), nil)
return
}
userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, cdnName, inf.User.UserName)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
return
}
if err := generateStoreDNSSECKeys(inf.Tx.Tx, cdnName, cdnDomain, uint64(*req.TTL), uint64(*req.KSKExpirationDays), uint64(*req.ZSKExpirationDays), int64(*req.EffectiveDateUnix), inf.Vault, r.Context()); err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating and storing DNSSEC CDN keys: "+err.Error()))
return
}
// NOTE: using a separate transaction (with its own timeout) for the changelog because the main
// transaction can time out if DNSSEC generation takes too long
db, err := api.GetDB(r.Context())
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating CDN DNSSEC keys: getting DB from request context for changelog: "+err.Error()))
return
}
logCtx, logCancel := context.WithTimeout(r.Context(), 30*time.Second)
defer logCancel()
logTx, err := db.BeginTxx(logCtx, nil)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating CDN DNSSEC keys: could not begin transaction for changelog: "+err.Error()))
return
}
defer func() {
if err := logTx.Commit(); err != nil && err != sql.ErrTxDone {
log.Errorln("generating CDN DNSSEC keys: committing transaction for changelog: " + err.Error())
}
}()
api.CreateChangeLogRawTx(api.ApiChange, "CDN: "+cdnName+", ID: "+strconv.Itoa(cdnID)+", ACTION: Generated DNSSEC keys", inf.User, logTx.Tx)
api.WriteResp(w, r, "Successfully created dnssec keys for "+cdnName)
}
// DefaultDSTTL is the default DS Record TTL to use, if no CDN Snapshot exists, or if no tld.ttls.DS parameter exists.
// This MUST be the same value as Traffic Router's default. Currently:
// traffic_router/core/src/main/java/org/apache/traffic_control/traffic_router/core/dns/SignatureManager.java:476
// `final Long dsTtl = ZoneUtils.getLong(config.get("ttls"), "DS", 60);`.
// If Traffic Router and Traffic Ops differ, and a user is using the default, errors may occur!
// Users are advised to set the tld.ttls.DS CRConfig.json Parameter, so the default is not used!
// Traffic Ops functions SHOULD warn whenever this default is used.
const DefaultDSTTL = 60 * time.Second
func GetDNSSECKeys(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
if !inf.Config.TrafficVaultEnabled {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys from Traffic Vault: Traffic Vault is not configured"))
return
}
cdnName := inf.Params["name"]
tvKeys, keysExist, err := inf.Vault.GetDNSSECKeys(cdnName, inf.Tx.Tx, r.Context())
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting DNSSEC CDN keys: "+err.Error()))
return
}
if !keysExist {
// TODO emulates Perl; change to error, 404?
api.WriteRespAlertObj(w, r, tc.SuccessLevel, " - Dnssec keys for "+cdnName+" could not be found. ", struct{}{}) // emulates Perl
return
}
dsTTL, err := GetDSRecordTTL(inf.Tx.Tx, cdnName)
if err != nil {
log.Errorln("Getting DNSSEC Keys: getting DS Record TTL from CRConfig Snapshot: " + err.Error())
log.Errorf("Getting DNSSEC Keys: getting DS Record TTL failed, using default %v. It is STRONGLY ADVISED to fix the error, and ensure a CRConfig Snapshot exists for the CDN, and a tld.ttls.DS CRConfig.json Parameter exists on a Router Profile on the CDN. Default DS Records may cause unexpected behavior or errors!\n", DefaultDSTTL)
dsTTL = DefaultDSTTL
}
keys, err := deliveryservice.MakeDNSSECKeysFromTrafficVaultKeys(tvKeys, dsTTL)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys object from Traffic Vault keys: "+err.Error()))
return
}
api.WriteResp(w, r, keys)
}
func GetDSRecordTTL(tx *sql.Tx, cdn string) (time.Duration, error) {
ttlSeconds := 0
if err := tx.QueryRow(`SELECT JSON_EXTRACT_PATH_TEXT(crconfig, 'config', 'ttls', 'DS') FROM snapshot WHERE cdn = $1`, cdn).Scan(&ttlSeconds); err != nil {
return 0, errors.New("getting cdn '" + cdn + "' DS Record TTL from CRConfig: " + err.Error())
}
return time.Duration(ttlSeconds) * time.Second, nil
}
func generateStoreDNSSECKeys(
tx *sql.Tx,
cdnName string,
cdnDomain string,
ttlSeconds uint64,
kExpDays uint64,
zExpDays uint64,
effectiveDateUnix int64,
tv trafficvault.TrafficVault,
ctx context.Context,
) error {
zExp := time.Duration(zExpDays) * time.Hour * 24
kExp := time.Duration(kExpDays) * time.Hour * 24
ttl := time.Duration(ttlSeconds) * time.Second
oldKeys, oldKeysExist, err := tv.GetDNSSECKeys(cdnName, tx, ctx)
if err != nil {
return errors.New("getting old dnssec keys: " + err.Error())
}
dses, err := GetCDNDeliveryServices(tx, cdnName)
if err != nil {
return errors.New("getting cdn delivery services: " + err.Error())
}
cdnDNSDomain := cdnDomain
if !strings.HasSuffix(cdnDNSDomain, ".") {
cdnDNSDomain = cdnDNSDomain + "."
}
cdnDNSDomain = strings.ToLower(cdnDNSDomain)
inception := time.Now()
newCDNZSK, err := deliveryservice.GetDNSSECKeysV11(tc.DNSSECZSKType, cdnDNSDomain, ttl, inception, inception.Add(zExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), false)
if err != nil {
return errors.New("creating zsk for cdn: " + err.Error())
}
newCDNKSK, err := deliveryservice.GetDNSSECKeysV11(tc.DNSSECKSKType, cdnDNSDomain, ttl, inception, inception.Add(kExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), true)
if err != nil {
return errors.New("creating ksk for cdn: " + err.Error())
}
newCDNZSKs := []tc.DNSSECKeyV11{newCDNZSK}
newCDNKSKs := []tc.DNSSECKeyV11{newCDNKSK}
if oldKeysExist {
oldKeyCDN, oldKeyCDNExists := oldKeys[cdnName]
if oldKeyCDNExists && len(oldKeyCDN.KSK) > 0 {
ksk := oldKeyCDN.KSK[0]
ksk.Status = DNSSECStatusExisting
ksk.TTLSeconds = uint64(ttl / time.Second)
ksk.ExpirationDateUnix = effectiveDateUnix
newCDNKSKs = append(newCDNKSKs, ksk)
}
if oldKeyCDNExists && len(oldKeyCDN.ZSK) > 0 {
zsk := oldKeyCDN.ZSK[0]
zsk.Status = DNSSECStatusExisting
zsk.TTLSeconds = uint64(ttl / time.Second)
zsk.ExpirationDateUnix = effectiveDateUnix
newCDNZSKs = append(newCDNZSKs, zsk)
}
}
newKeys := tc.DNSSECKeysTrafficVault{}
newKeys[cdnName] = tc.DNSSECKeySetV11{ZSK: newCDNZSKs, KSK: newCDNKSKs}
cdnKeys := newKeys[cdnName]
dsNames := []string{}
for _, ds := range dses {
dsNames = append(dsNames, ds.Name)
}
matchLists, err := deliveryservice.GetDeliveryServicesMatchLists(dsNames, tx)
if err != nil {
return errors.New("getting delivery service matchlists: " + err.Error())
}
jobList := make([]dnssecGenJob, 0, len(dses))
for _, ds := range dses {
if !ds.Type.IsHTTP() && !ds.Type.IsDNS() {
continue // skip delivery services that aren't DNS or HTTP (e.g. ANY_MAP)
}
matchlist, ok := matchLists[ds.Name]
if !ok {
return errors.New("no regex match list found for delivery service '" + ds.Name)
}
exampleURLs := deliveryservice.MakeExampleURLs(ds.Protocol, ds.Type, ds.RoutingName, matchlist, cdnDomain)
jobList = append(jobList, dnssecGenJob{
XMLID: ds.Name,
ExampleURLs: exampleURLs,
CDNKeys: cdnKeys,
KExp: kExp,
ZExp: zExp,
TTL: ttl,
OverrideTTL: true,
})
}
numWorkers := int(math.Max(1, math.Floor(float64(runtime.NumCPU())*DNSSECGenerationCPURatio)))
jobChan := make(chan dnssecGenJob, len(jobList))
resultChan := make(chan dnssecGenResult, len(jobList))
panickedChan := make(chan struct{}, numWorkers)
wg := sync.WaitGroup{}
wg.Add(numWorkers)
for w := 0; w < numWorkers; w++ {
go dnssecGenWorker(w, &wg, jobChan, resultChan, panickedChan)
}
for _, j := range jobList {
jobChan <- j
}
close(jobChan)
wg.Wait()
select {
case <-panickedChan:
return errors.New("creating DNSSEC keys, at least one worker goroutine panicked")
default:
log.Infoln("no DNSSEC generation worker goroutines panicked")
}
for i := 0; i < len(jobList); i++ {
res := <-resultChan
if res.Error != nil {
return fmt.Errorf("creating DNSSEC keys for delivery service %s: %s", res.XMLID, res.Error.Error())
}
newKeys[res.XMLID] = *res.Keys
}
if err := tv.PutDNSSECKeys(cdnName, newKeys, tx, ctx); err != nil {
return errors.New("putting CDN DNSSEC keys in Traffic Vault: " + err.Error())
}
return nil
}
func dnssecGenWorker(id int, waitGroup *sync.WaitGroup, jobs <-chan dnssecGenJob, results chan<- dnssecGenResult, panicked chan<- struct{}) {
log.Infof("DNSSEC gen worker %d starting", id)
defer func() {
if r := recover(); r != nil {
panicked <- struct{}{}
log.Errorf("DNSSEC gen worker %d recovered from panic: %v", id, r)
}
waitGroup.Done()
log.Infof("DNSSEC gen worker %d exiting", id)
}()
for j := range jobs {
log.Infof("DNSSEC gen worker %d creating keys for %s", id, j.XMLID)
res := dnssecGenResult{XMLID: j.XMLID}
dsKeys, err := deliveryservice.CreateDNSSECKeys(j.ExampleURLs, j.CDNKeys, j.KExp, j.ZExp, j.TTL, j.OverrideTTL)
if err != nil {
res.Error = err
} else {
res.Keys = &dsKeys
}
results <- res
}
}
type dnssecGenJob struct {
XMLID string
ExampleURLs []string
CDNKeys tc.DNSSECKeySetV11
KExp time.Duration
ZExp time.Duration
TTL time.Duration
OverrideTTL bool
}
type dnssecGenResult struct {
XMLID string
Keys *tc.DNSSECKeySetV11
Error error
}
const API_DNSSECKEYS = "DELETE /cdns/name/:name/dnsseckeys"
type CDNDS struct {
Name string
Protocol *int
Type tc.DSType
RoutingName string
}
// getCDNDeliveryServices returns basic data for the delivery services on the given CDN, as well as the CDN name, or any error.
func GetCDNDeliveryServices(tx *sql.Tx, cdn string) ([]CDNDS, error) {
q := `
SELECT ds.xml_id, ds.protocol, t.name as type, ds.routing_name
FROM deliveryservice as ds
JOIN cdn ON ds.cdn_id = cdn.id
JOIN type as t ON ds.type = t.id
WHERE cdn.name = $1
`
rows, err := tx.Query(q, cdn)
if err != nil {
return nil, errors.New("getting cdn delivery services: " + err.Error())
}
defer rows.Close()
dses := []CDNDS{}
for rows.Next() {
ds := CDNDS{}
dsTypeStr := ""
if err := rows.Scan(&ds.Name, &ds.Protocol, &dsTypeStr, &ds.RoutingName); err != nil {
return nil, errors.New("scanning cdn delivery services: " + err.Error())
}
dsType := tc.DSTypeFromString(dsTypeStr)
if dsType == tc.DSTypeInvalid {
return nil, errors.New("got invalid delivery service type '" + dsTypeStr + "'")
}
ds.Type = dsType
dses = append(dses, ds)
}
return dses, nil
}
func DeleteDNSSECKeys(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
if !inf.Config.TrafficVaultEnabled {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys from Traffic Vault: Traffic Vault is not configured"))
return
}
key := inf.Params["name"]
cdnID, ok, err := getCDNIDFromName(inf.Tx.Tx, tc.CDNName(key))
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cdn id: "+err.Error()))
return
} else if !ok {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil)
return
}
userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, key, inf.User.UserName)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
return
}
if err := inf.Vault.DeleteDNSSECKeys(key, inf.Tx.Tx, r.Context()); err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting CDN DNSSEC keys: "+err.Error()))
return
}
api.CreateChangeLogRawTx(api.ApiChange, "CDN: "+key+", ID: "+strconv.Itoa(cdnID)+", ACTION: Deleted DNSSEC keys", inf.User, inf.Tx.Tx)
successMsg := "Successfully deleted " + CDNDNSSECKeyType + " for " + key
api.WriteResp(w, r, successMsg)
}
// getCDNIDFromName returns the CDN's ID if a CDN with the given name exists
func getCDNIDFromName(tx *sql.Tx, name tc.CDNName) (int, bool, error) {
id := 0
if err := tx.QueryRow(`SELECT id FROM cdn WHERE name = $1`, name).Scan(&id); err != nil {
if err == sql.ErrNoRows {
return id, false, nil
}
return id, false, errors.New("querying CDN ID from name: " + err.Error())
}
return id, true, nil
}