1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00
Phil Pennock 3c41a39252 BIND: Implement AutoDNSSEC (#648)
There's a philosophy issue here around what is the Bind output meant to
do.  Since AFAIK we're not integrating into Bind's catalog zones or the
like, we're just targeting the zonefiles, we're not in a position to do
_anything_ relating to registrar options such as setting up DS glue.

So at one level, enabling AutoDNSSEC for Bind is a lie. But without
this, folks can't target a Bind zone as a secondary provider for their
domain, to get debug dumps of the zone output, because the checks for
"Can" block it.  So I think this commit achieves a happy compromise: we
write a comment into the Bind zonefile, indicating that DNSSEC was
requested.

Actually: we add support for arbitrary zone comments to be written into
a zonefile via a slightly ugly "can be `nil`" parameter.  We then write
in a generation timestamp comment, and if AutoDNSSEC was requested we
then write that in too.
2020-02-22 13:27:24 -05:00

211 lines
6.0 KiB
Go

package commands
import (
"fmt"
"os"
"strings"
"github.com/StackExchange/dnscontrol/v2/models"
"github.com/StackExchange/dnscontrol/v2/pkg/prettyzone"
"github.com/StackExchange/dnscontrol/v2/providers"
"github.com/StackExchange/dnscontrol/v2/providers/config"
"github.com/urfave/cli/v2"
)
var _ = cmd(catUtils, func() *cli.Command {
var args GetZoneArgs
return &cli.Command{
Name: "get-zones",
Aliases: []string{"get-zone"},
Usage: "gets a zone from a provider (stand-alone)",
Action: func(ctx *cli.Context) error {
if ctx.NArg() < 3 {
return cli.NewExitError("Arguments should be: credskey providername zone(s) (Ex: r53 ROUTE53 example.com)", 1)
}
args.CredName = ctx.Args().Get(0)
args.ProviderName = ctx.Args().Get(1)
args.ZoneNames = ctx.Args().Slice()[2:]
return exit(GetZone(args))
},
Flags: args.flags(),
UsageText: "dnscontrol get-zones [command options] credkey provider zone [...]",
Description: `Download a zone from a provider. This is a stand-alone utility.
ARGUMENTS:
credkey: The name used in creds.json (first parameter to NewDnsProvider() in dnsconfig.js)
provider: The name of the provider (second parameter to NewDnsProvider() in dnsconfig.js)
zone: One or more zones (domains) to download; or "all".
FORMATS:
--format=dsl dnsconfig.js format (not perfect, but a decent first draft)
--format=nameonly Just print the zone names
--format=pretty BIND Zonefile format
--format=tsv TAB separated value (useful for AWK)
EXAMPLES:
dnscontrol get-zones myr53 ROUTE53 example.com
dnscontrol get-zones gmain GANDI_V5 example.comn other.com
dnscontrol get-zones cfmain CLOUDFLAREAPI all
dnscontrol get-zones -format=tsv bind BIND example.com
dnscontrol get-zones -format=dsl -out=draft.js glcoud GCLOUD example.com`,
}
}())
// GetZoneArgs args required for the create-domain subcommand.
type GetZoneArgs struct {
GetCredentialsArgs // Args related to creds.json
CredName string // key in creds.json
ProviderName string // provider name: BIND, GANDI_V5, etc or "-"
ZoneNames []string // The zones to get
OutputFormat string // Output format
OutputFile string // Filename to send output ("" means stdout)
DefaultTTL int // default TTL for providers where it is unknown
}
func (args *GetZoneArgs) flags() []cli.Flag {
flags := args.GetCredentialsArgs.flags()
flags = append(flags, &cli.StringFlag{
Name: "format",
Destination: &args.OutputFormat,
Value: "pretty",
Usage: `Output format: dsl pretty tsv nameonly`,
})
flags = append(flags, &cli.StringFlag{
Name: "out",
Destination: &args.OutputFile,
Usage: `Instead of stdout, write to this file`,
})
flags = append(flags, &cli.IntFlag{
Name: "ttl",
Destination: &args.DefaultTTL,
Usage: `Default TTL`,
Value: 300,
})
return flags
}
// GetZone contains all data/flags needed to run get-zones, independently of CLI.
func GetZone(args GetZoneArgs) error {
var providerConfigs map[string]map[string]string
var err error
// Read it in:
providerConfigs, err = config.LoadProviderConfigs(args.CredsFile)
if err != nil {
return err
}
provider, err := providers.CreateDNSProvider(args.ProviderName, providerConfigs[args.CredName], nil)
if err != nil {
return err
}
// decide which zones we need to convert
zones := args.ZoneNames
if len(args.ZoneNames) == 1 && args.ZoneNames[0] == "all" {
lister, ok := provider.(providers.ZoneLister)
if !ok {
return fmt.Errorf("provider type %s cannot list zones to use the 'all' feature", args.ProviderName)
}
zones, err = lister.ListZones()
if err != nil {
return err
}
}
// first open output stream and print initial header (if applicable)
w := os.Stdout
if args.OutputFile != "" {
w, err = os.Create(args.OutputFile)
}
if err != nil {
return err
}
defer w.Close()
if args.OutputFormat == "nameonly" {
for _, zone := range zones {
fmt.Fprintln(w, zone)
}
return nil
}
// actually fetch all of the records
zoneRecs := make([]models.Records, len(zones))
for i, zone := range zones {
recs, err := provider.GetZoneRecords(zone)
if err != nil {
return err
}
zoneRecs[i] = recs
}
// Write it out:
if args.OutputFormat == "dsl" {
fmt.Fprintf(w, `var %s = NewDnsProvider("%s", "%s");`+"\n",
args.CredName, args.CredName, args.ProviderName)
}
// now print all zones
for i, recs := range zoneRecs {
zoneName := zones[i]
z := prettyzone.PrettySort(recs, zoneName, 0, nil)
switch args.OutputFormat {
case "pretty":
fmt.Fprintf(w, "$ORIGIN %s.\n", zoneName)
prettyzone.WriteZoneFileRC(w, z.Records, zoneName, nil)
fmt.Fprintln(w)
case "dsl":
fmt.Fprintf(w, `D("%s", REG_CHANGEME,`+"\n", zoneName)
fmt.Fprintf(w, "\tDnsProvider(%s)", args.CredName)
for _, rec := range recs {
fmt.Fprint(w, formatDsl(zoneName, rec, uint32(args.DefaultTTL)))
}
fmt.Fprint(w, "\n)\n")
case "tsv":
for _, rec := range recs {
fmt.Fprintf(w,
fmt.Sprintf("%s\t%s\t%d\tIN\t%s\t%s\n",
rec.NameFQDN, rec.Name, rec.TTL, rec.Type, rec.GetTargetCombined()))
}
default:
return fmt.Errorf("format %q unknown", args.OutputFile)
}
}
return nil
}
func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) string {
target := rec.GetTargetCombined()
ttlop := ""
if rec.TTL != defaultTTL && rec.TTL != 0 {
ttlop = fmt.Sprintf(", TTL(%d)", rec.TTL)
}
switch rec.Type { // #rtype_variations
case "MX":
target = fmt.Sprintf("%d, '%s'", rec.MxPreference, rec.GetTargetField())
case "SOA":
case "TXT":
if len(rec.TxtStrings) == 1 {
target = `'` + rec.TxtStrings[0] + `'`
} else {
target = `['` + strings.Join(rec.TxtStrings, `', '`) + `']`
}
case "NS":
// NS records at the apex should be NAMESERVER() records.
if rec.Name == "@" {
return fmt.Sprintf(",\n\tNAMESERVER('%s'%s)", target, ttlop)
}
default:
target = "'" + target + "'"
}
return fmt.Sprintf(",\n\t%s('%s', %s%s)", rec.Type, rec.Name, target, ttlop)
}