2021-06-22 22:24:49 +08:00
|
|
|
package akamaiedgedns
|
|
|
|
|
|
|
|
/*
|
|
|
|
For information about Akamai's "Edge DNS Zone Management API", see:
|
|
|
|
https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html
|
|
|
|
|
|
|
|
For information about "AkamaiOPEN-edgegrid-golang" library, see:
|
|
|
|
https://github.com/akamai/AkamaiOPEN-edgegrid-golang
|
|
|
|
*/
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
|
|
dnsv2 "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
|
|
|
|
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
|
|
|
|
)
|
|
|
|
|
|
|
|
// initialize initializes the "Akamai OPEN EdgeGrid" library
|
|
|
|
func initialize(clientSecret string, host string, accessToken string, clientToken string) {
|
|
|
|
|
|
|
|
eg := edgegrid.Config{
|
|
|
|
ClientSecret: clientSecret,
|
|
|
|
Host: host,
|
|
|
|
AccessToken: accessToken,
|
|
|
|
ClientToken: clientToken,
|
|
|
|
MaxBody: 131072,
|
|
|
|
Debug: false,
|
|
|
|
}
|
|
|
|
dnsv2.Init(eg)
|
|
|
|
}
|
|
|
|
|
|
|
|
// zoneDoesExist returns true if the zone exists, false otherwise.
|
|
|
|
func zoneDoesExist(zonename string) bool {
|
|
|
|
_, err := dnsv2.GetZone(zonename)
|
|
|
|
return err == nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createZone create a new zone and creates SOA and NS records for the zone.
|
|
|
|
// Akamai assigns a unique set of authoritative nameservers for each contract. These authorities should be
|
|
|
|
// used as the NS records on all zones belonging to this contract.
|
|
|
|
func createZone(zonename string, contractID string, groupID string) error {
|
|
|
|
zone := &dnsv2.ZoneCreate{
|
|
|
|
Zone: zonename,
|
|
|
|
Type: "PRIMARY",
|
|
|
|
Comment: "This zone created by DNSControl (http://dnscontrol.org)",
|
|
|
|
SignAndServe: false,
|
|
|
|
SignAndServeAlgorithm: "RSA_SHA512",
|
|
|
|
ContractId: contractID,
|
|
|
|
}
|
|
|
|
|
|
|
|
queryArgs := &dnsv2.ZoneQueryString{
|
|
|
|
Contract: contractID,
|
|
|
|
Group: groupID,
|
|
|
|
}
|
|
|
|
|
|
|
|
err := dnsv2.ValidateZone(zone)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("invalid value provided for zone. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
err = zone.Save(*queryArgs)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("zone create failed. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Indirectly create NS and SOA records
|
|
|
|
err = zone.SaveChangelist()
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("zone initialization failed. SOA and NS records need to be created")
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
err = zone.SubmitChangelist()
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("zone create failed. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
printer.Printf("Created zone: %s\n", zone.Zone)
|
|
|
|
printer.Printf(" Type: %s\n", zone.Type)
|
|
|
|
printer.Printf(" Comment: %s\n", zone.Comment)
|
|
|
|
printer.Printf(" SignAndServe: %v\n", zone.SignAndServe)
|
|
|
|
printer.Printf(" SignAndServeAlgorithm: %s\n", zone.SignAndServeAlgorithm)
|
|
|
|
printer.Printf(" ContractId: %s\n", zone.ContractId)
|
|
|
|
printer.Printf(" GroupId: %s\n", queryArgs.Group)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// listZones lists all zones associated with this contract.
|
|
|
|
func listZones(contractID string) ([]string, error) {
|
|
|
|
queryArgs := dnsv2.ZoneListQueryArgs{
|
|
|
|
ContractIds: contractID,
|
|
|
|
ShowAll: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
zoneListResp, err := dnsv2.ListZones(queryArgs)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return nil, fmt.Errorf("zone list retrieval failed. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
edgeDNSZones := zoneListResp.Zones // what we have
|
|
|
|
var zones []string // what we return
|
|
|
|
|
|
|
|
for _, edgeDNSZone := range edgeDNSZones {
|
|
|
|
zones = append(zones, edgeDNSZone.Zone)
|
|
|
|
}
|
|
|
|
|
|
|
|
return zones, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// isAutoDNSSecEnabled returns true if AutoDNSSEC (SignAndServe) is enabled, false otherwise.
|
|
|
|
func isAutoDNSSecEnabled(zonename string) (bool, error) {
|
|
|
|
zone, err := dnsv2.GetZone(zonename)
|
|
|
|
if err != nil {
|
|
|
|
if dnsv2.IsConfigDNSError(err) && err.(dnsv2.ConfigDNSError).NotFound() {
|
2021-12-24 04:16:37 +08:00
|
|
|
return false, fmt.Errorf("zone %s does not exist. error: %s",
|
2021-06-22 22:24:49 +08:00
|
|
|
zonename, err.Error())
|
|
|
|
}
|
2021-12-24 04:16:37 +08:00
|
|
|
return false, fmt.Errorf("error retrieving information for zone %s. error: %s",
|
2021-06-22 22:24:49 +08:00
|
|
|
zonename, err.Error())
|
|
|
|
}
|
|
|
|
return zone.SignAndServe, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// autoDNSSecEnable enables or disables AutoDNSSEC (SignAndServe) for the zone.
|
|
|
|
func autoDNSSecEnable(enable bool, zonename string) error {
|
|
|
|
zone, err := dnsv2.GetZone(zonename)
|
|
|
|
if err != nil {
|
|
|
|
if dnsv2.IsConfigDNSError(err) && err.(dnsv2.ConfigDNSError).NotFound() {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("zone %s does not exist. error: %s",
|
2021-06-22 22:24:49 +08:00
|
|
|
zonename, err.Error())
|
|
|
|
}
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("error retrieving information for zone %s. error: %s",
|
2021-06-22 22:24:49 +08:00
|
|
|
zonename, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
algorithm := "RSA_SHA512"
|
|
|
|
if zone.SignAndServeAlgorithm != "" {
|
|
|
|
algorithm = zone.SignAndServeAlgorithm
|
|
|
|
}
|
|
|
|
|
|
|
|
modifiedzone := &dnsv2.ZoneCreate{
|
|
|
|
Zone: zone.Zone,
|
|
|
|
Type: zone.Type,
|
|
|
|
Masters: zone.Masters,
|
|
|
|
Comment: zone.Comment,
|
|
|
|
SignAndServe: enable, // AutoDNSSEC
|
|
|
|
SignAndServeAlgorithm: algorithm,
|
|
|
|
TsigKey: zone.TsigKey,
|
|
|
|
EndCustomerId: zone.EndCustomerId,
|
|
|
|
ContractId: zone.ContractId,
|
|
|
|
}
|
|
|
|
|
|
|
|
queryArgs := dnsv2.ZoneQueryString{}
|
|
|
|
|
|
|
|
err = modifiedzone.Update(queryArgs)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("error updating zone %s. error: %s",
|
2021-06-22 22:24:49 +08:00
|
|
|
zonename, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getAuthorities returns the list of authoritative nameservers for the contract.
|
|
|
|
// Akamai assigns a unique set of authoritative nameservers for each contract. These authorities should be
|
|
|
|
// used as the NS records on all zones belonging to this contract.
|
|
|
|
func getAuthorities(contractID string) ([]string, error) {
|
|
|
|
authorityResponse, err := dnsv2.GetAuthorities(contractID)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return nil, fmt.Errorf("getAuthorities - contractid %s: authorities retrieval failed. Error: %s",
|
2021-06-22 22:24:49 +08:00
|
|
|
contractID, err.Error())
|
|
|
|
}
|
|
|
|
contracts := authorityResponse.Contracts
|
|
|
|
if len(contracts) != 1 {
|
2021-12-24 04:16:37 +08:00
|
|
|
return nil, fmt.Errorf("getAuthorities - contractid %s: Expected 1 element in array but got %d",
|
2021-06-22 22:24:49 +08:00
|
|
|
contractID, len(contracts))
|
|
|
|
}
|
|
|
|
cid := contracts[0].ContractID
|
|
|
|
if cid != contractID {
|
2021-12-24 04:16:37 +08:00
|
|
|
return nil, fmt.Errorf("getAuthorities - contractID %s: got authorities for wrong contractID (%s)",
|
2021-06-22 22:24:49 +08:00
|
|
|
contractID, cid)
|
|
|
|
}
|
|
|
|
authorities := contracts[0].Authorities
|
|
|
|
return authorities, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// rcToRs converts DNSControl RecordConfig records to an AkamaiEdgeDNS recordset.
|
|
|
|
func rcToRs(records []*models.RecordConfig, zonename string) (*dnsv2.RecordBody, error) {
|
|
|
|
if len(records) == 0 {
|
2021-12-24 04:16:37 +08:00
|
|
|
return nil, fmt.Errorf("no records to replace")
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
akaRecord := &dnsv2.RecordBody{
|
|
|
|
Name: records[0].NameFQDN,
|
|
|
|
RecordType: records[0].Type,
|
|
|
|
TTL: int(records[0].TTL),
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, r := range records {
|
|
|
|
akaRecord.Target = append(akaRecord.Target, r.GetTargetCombined())
|
|
|
|
}
|
|
|
|
|
|
|
|
return akaRecord, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createRecordset creates a new AkamaiEdgeDNS recordset in the zone.
|
|
|
|
func createRecordset(records []*models.RecordConfig, zonename string) error {
|
|
|
|
akaRecord, err := rcToRs(records, zonename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = akaRecord.Save(zonename, true)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("recordset creation failed. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// replaceRecordset replaces an existing AkamaiEdgeDNS recordset in the zone.
|
|
|
|
func replaceRecordset(records []*models.RecordConfig, zonename string) error {
|
|
|
|
akaRecord, err := rcToRs(records, zonename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = akaRecord.Update(zonename, true)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("recordset update failed. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// deleteRecordset deletes an existing AkamaiEdgeDNS recordset in the zone.
|
|
|
|
func deleteRecordset(records []*models.RecordConfig, zonename string) error {
|
|
|
|
akaRecord, err := rcToRs(records, zonename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = akaRecord.Delete(zonename, true)
|
|
|
|
if err != nil {
|
|
|
|
if dnsv2.IsConfigDNSError(err) && err.(dnsv2.ConfigDNSError).NotFound() {
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("recordset not found")
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
2021-12-24 04:16:37 +08:00
|
|
|
return fmt.Errorf("failed to delete recordset. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Example AkamaiEdgeDNS Recordset (as JSON):
|
|
|
|
{
|
|
|
|
"name": "test.com",
|
|
|
|
"rdata": [
|
|
|
|
"a7.akafp.net.",
|
|
|
|
"a4.akafp.net.",
|
|
|
|
"a0.akafp.net."
|
|
|
|
],
|
|
|
|
"ttl": 10000,
|
|
|
|
"type": "NS"
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
// getRecords returns all RecordConfig records in the zone.
|
|
|
|
func getRecords(zonename string) ([]*models.RecordConfig, error) {
|
|
|
|
queryArgs := dnsv2.RecordsetQueryArgs{ShowAll: true}
|
|
|
|
|
|
|
|
rsetResp, err := dnsv2.GetRecordsets(zonename, queryArgs)
|
|
|
|
if err != nil {
|
2021-12-24 04:16:37 +08:00
|
|
|
return nil, fmt.Errorf("recordset list retrieval failed. error: %s", err.Error())
|
2021-06-22 22:24:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
akaRecordsets := rsetResp.Recordsets // what we have
|
|
|
|
var recordConfigs []*models.RecordConfig // what we return
|
|
|
|
|
|
|
|
// For each AkamaiEdgeDNS recordset...
|
|
|
|
for _, akarecset := range akaRecordsets {
|
|
|
|
akaname := akarecset.Name
|
|
|
|
akatype := akarecset.Type
|
|
|
|
akattl := akarecset.TTL
|
|
|
|
|
|
|
|
// Don't report the existence of an SOA record (because DnsControl will try to delete the SOA record).
|
|
|
|
if akatype == "SOA" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// ... convert the recordset into 1 or more RecordConfig structs
|
|
|
|
for _, r := range akarecset.Rdata {
|
|
|
|
rc := &models.RecordConfig{
|
|
|
|
Type: akatype,
|
|
|
|
TTL: uint32(akattl),
|
|
|
|
}
|
|
|
|
rc.SetLabelFromFQDN(akaname, zonename)
|
|
|
|
err = rc.PopulateFromString(akatype, r, zonename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
recordConfigs = append(recordConfigs, rc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return recordConfigs, nil
|
|
|
|
}
|