1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

NEW PROVIDER: MSDNS (#1005)

* New provider
* Add support for SRV records
* Modify ACTIVEDIRECTORY_PS provider to warn that it is deprecated.
This commit is contained in:
Tom Limoncelli
2020-12-28 16:07:33 -05:00
committed by GitHub
parent 38e3e706cd
commit 50db086278
19 changed files with 1206 additions and 24 deletions

122
providers/msdns/convert.go Normal file
View File

@@ -0,0 +1,122 @@
package msdns
// Convert the provider's native record description to models.RecordConfig.
import (
"encoding/json"
"fmt"
"net"
"github.com/StackExchange/dnscontrol/v3/models"
)
// extractProps and collects Name/Value pairs into maps for easier access.
func extractProps(cip []ciProperty) (map[string]string, map[string]uint32, error) {
// Sadly this structure is dynamic JSON i.e. .Value could be an int, string,
// or a map. We peek at the first byte to guess at the contents.
// We store strings in sprops, numbers in uprops. Maps are special: Currently
// the only map we decode is a map with the same duration in many units. We
// simply pick the Seconds unit and store it as a number.
sprops := map[string]string{}
uprops := map[string]uint32{}
for _, p := range cip {
name := p.Name
if len(p.Value) == 0 {
// Empty string? Skip it.
} else if p.Value[0] == '"' {
// First byte is a quote. Must be a string.
var svalue string
err := json.Unmarshal(p.Value, &svalue)
if err != nil {
return nil, nil, fmt.Errorf("could not unmarshal string value=%q: %w", p.Value, err)
}
sprops[name] = svalue
} else if p.Value[0] == '{' {
// First byte is {. Must be a map.
var dvalue ciValueDuration
err := json.Unmarshal(p.Value, &dvalue)
if err != nil {
return nil, nil, fmt.Errorf("could not unmarshal duration value=%q: %w", p.Value, err)
}
uprops[name] = uint32(dvalue.TotalSeconds)
} else {
// Assume it is a number.
var uvalue uint32
err := json.Unmarshal(p.Value, &uvalue)
if err != nil {
return nil, nil, fmt.Errorf("could not unmarshal uint value=%q: %w", p.Value, err)
}
uprops[name] = uvalue
}
}
return sprops, uprops, nil
}
// nativeToRecord takes a DNS record from DNS and returns a native RecordConfig struct.
func nativeToRecords(nr nativeRecord, origin string) (*models.RecordConfig, error) {
rc := &models.RecordConfig{
Type: nr.RecordType,
Original: nr,
}
rc.SetLabel(nr.HostName, origin)
rc.TTL = uint32(nr.TimeToLive.TotalSeconds)
sprops, uprops, err := extractProps(nr.RecordData.CimInstanceProperties)
if err != nil {
return nil, err
}
switch rtype := nr.RecordType; rtype {
case "A":
contents := sprops["IPv4Address"]
ip := net.ParseIP(contents)
if ip == nil || ip.To4() == nil {
return nil, fmt.Errorf("invalid IP in A record: %q", contents)
}
rc.SetTargetIP(ip)
case "AAAA":
contents := sprops["IPv6Address"]
ip := net.ParseIP(contents)
if ip == nil || ip.To16() == nil {
return nil, fmt.Errorf("invalid IPv6 in AAAA record: %q", contents)
}
rc.SetTargetIP(ip)
case "CNAME":
rc.SetTarget(sprops["HostNameAlias"])
case "MX":
rc.SetTargetMX(uint16(uprops["Preference"]), sprops["MailExchange"])
case "NS":
rc.SetTarget(sprops["NameServer"])
case "PTR":
rc.SetTarget(sprops["PtrDomainName"])
case "SRV":
rc.SetTargetSRV(
uint16(uprops["Priority"]),
uint16(uprops["Weight"]),
uint16(uprops["Port"]),
sprops["DomainName"],
)
case "SOA":
// We discard SOA records for now. Windows DNS doesn't let us delete
// them and they get in the way of integration tests. In the future,
// we should support SOA records by (1) ignoring them in the
// integration tests. (2) generatePSModify will have to special-case
// updates.
return nil, nil
// If we weren't ignoring them, the code would look like this:
//rc.SetTargetSOA(sprops["PrimaryServer"], sprops["ResponsiblePerson"],
// uprops["SerialNumber"], uprops["RefreshInterval"], uprops["RetryDelay"],
// uprops["ExpireLimit"], uprops["MinimumTimeToLive"])
case "TXT":
rc.SetTargetTXTString(sprops["DescriptiveText"])
default:
return nil, fmt.Errorf(
"msdns/convert.go:nativeToRecord rtype=%q unknown: props=%+v and %+v",
rtype, sprops, uprops)
}
return rc, nil
}

View File

@@ -0,0 +1,72 @@
package msdns
import (
"fmt"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
)
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
func (c *msdnsProvider) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
// Read foundRecords:
foundRecords, err := c.GetZoneRecords(dc.Name)
if err != nil {
return nil, fmt.Errorf("c.GetDNSZoneRecords(%v) failed: %v", dc.Name, err)
}
// Normalize
models.PostProcessRecords(foundRecords)
differ := diff.New(dc)
_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)
if err != nil {
return nil, err
}
// Generate changes.
corrections := []*models.Correction{}
for _, del := range dels {
corrections = append(corrections, c.deleteRec(c.dnsserver, dc.Name, del))
}
for _, cre := range creates {
corrections = append(corrections, c.createRec(c.dnsserver, dc.Name, cre)...)
}
for _, m := range modifications {
corrections = append(corrections, c.modifyRec(c.dnsserver, dc.Name, m))
}
return corrections, nil
}
func (c *msdnsProvider) deleteRec(dnsserver, domainname string, cor diff.Correlation) *models.Correction {
rec := cor.Existing
return &models.Correction{
Msg: cor.String(),
F: func() error {
return c.shell.RecordDelete(dnsserver, domainname, rec)
},
}
}
func (c *msdnsProvider) createRec(dnsserver, domainname string, cre diff.Correlation) []*models.Correction {
rec := cre.Desired
arr := []*models.Correction{{
Msg: cre.String(),
F: func() error {
return c.shell.RecordCreate(dnsserver, domainname, rec)
},
}}
return arr
}
func (c *msdnsProvider) modifyRec(dnsserver, domainname string, m diff.Correlation) *models.Correction {
old, rec := m.Existing, m.Desired
return &models.Correction{
Msg: m.String(),
F: func() error {
return c.shell.RecordModify(dnsserver, domainname, old, rec)
},
}
}

View File

@@ -0,0 +1,8 @@
package msdns
import "github.com/StackExchange/dnscontrol/v3/models"
func (c *msdnsProvider) GetNameservers(string) ([]*models.Nameserver, error) {
// TODO: If using AD for publicly hosted zones, probably pull these from config.
return nil, nil
}

View File

@@ -0,0 +1,9 @@
package msdns
func (c *msdnsProvider) ListZones() ([]string, error) {
zones, err := c.shell.GetDNSServerZoneAll(c.dnsserver)
if err != nil {
return nil, err
}
return zones, err
}

View File

@@ -0,0 +1,129 @@
package msdns
import (
"encoding/json"
"fmt"
"runtime"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/providers"
)
// This is the struct that matches either (or both) of the Registrar and/or DNSProvider interfaces:
type msdnsProvider struct {
dnsserver string // Which DNS Server to update
pssession string // Remote machine to PSSession to
shell DNSAccessor // Handle for
}
var features = providers.DocumentationNotes{
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Cannot(),
providers.CanUseDS: providers.Unimplemented(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Unimplemented(),
providers.CanUseTXTMulti: providers.Unimplemented(),
providers.DocCreateDomains: providers.Cannot("This provider assumes the zone already existing on the dns server"),
providers.DocDualHost: providers.Cannot("This driver does not manage NS records, so should not be used for dual-host scenarios"),
providers.DocOfficiallySupported: providers.Can(),
}
// Register with the dnscontrol system.
// This establishes the name (all caps), and the function to call to initialize it.
func init() {
providers.RegisterDomainServiceProviderType("MSDNS", newDNS, features)
}
func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
if runtime.GOOS != "windows" {
fmt.Println("INFO: PowerShell not available. Disabling Active Directory provider.")
return providers.None{}, nil
}
var err error
p := &msdnsProvider{
dnsserver: config["dnsserver"],
}
p.shell, err = newPowerShell(config)
if err != nil {
return nil, err
}
return p, nil
}
// Section 3: Domain Service Provider (DSP) related functions
// NB(tal): To future-proof your code, all new providers should
// implement GetDomainCorrections exactly as you see here
// (byte-for-byte the same). In 3.0
// we plan on using just the individual calls to GetZoneRecords,
// PostProcessRecords, and so on.
//
// Currently every provider does things differently, which prevents
// us from doing things like using GetZoneRecords() of a provider
// to make convertzone work with all providers.
// GetDomainCorrections get the current and existing records,
// post-process them, and generate corrections.
func (client *msdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
existing, err := client.GetZoneRecords(dc.Name)
if err != nil {
return nil, err
}
models.PostProcessRecords(existing)
clean := PrepFoundRecords(existing)
PrepDesiredRecords(dc)
return client.GenerateDomainCorrections(dc, clean)
}
// GetZoneRecords gathers the DNS records and converts them to
// dnscontrol's format.
func (client *msdnsProvider) GetZoneRecords(domain string) (models.Records, error) {
// Get the existing DNS records in native format.
nativeExistingRecords, err := client.shell.GetDNSZoneRecords(client.dnsserver, domain)
if err != nil {
return nil, err
}
// Convert them to DNScontrol's native format:
existingRecords := make([]*models.RecordConfig, 0, len(nativeExistingRecords))
for _, rr := range nativeExistingRecords {
rc, err := nativeToRecords(rr, domain)
if err != nil {
return nil, err
}
if rc != nil {
existingRecords = append(existingRecords, rc)
}
}
return existingRecords, nil
}
// PrepFoundRecords munges any records to make them compatible with
// this provider. Usually this is a no-op.
func PrepFoundRecords(recs models.Records) models.Records {
// If there are records that need to be modified, removed, etc. we
// do it here. Usually this is a no-op.
return recs
}
// PrepDesiredRecords munges any records to best suit this provider.
func PrepDesiredRecords(dc *models.DomainConfig) {
// Sort through the dc.Records, eliminate any that can't be
// supported; modify any that need adjustments to work with the
// provider. We try to do minimal changes otherwise it gets
// confusing.
dc.Punycode()
}
// NB(tlim): If we want to implement a registrar, refer to
// http://go.microsoft.com/fwlink/?LinkId=288158
// (Get-DnsServerZoneDelegation) for hints about which PowerShell
// commands to use.

View File

@@ -0,0 +1,414 @@
package msdns
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/TomOnTime/utfutil"
ps "github.com/bhendo/go-powershell"
"github.com/bhendo/go-powershell/backend"
"github.com/bhendo/go-powershell/middleware"
)
type psHandle struct {
shell ps.Shell
}
func newPowerShell(config map[string]string) (*psHandle, error) {
back := &backend.Local{}
sh, err := ps.New(back)
if err != nil {
return nil, err
}
shell := sh
pssession := config["pssession"]
if pssession != "" {
fmt.Printf("INFO: PowerShell commands will run on %q\n", pssession)
// create a remote shell by wrapping the existing one in the session middleware
mconfig := middleware.NewSessionConfig()
mconfig.ComputerName = pssession
session, err := middleware.NewSession(sh, mconfig)
if err != nil {
panic(err)
}
shell = session
}
psh := &psHandle{
shell: shell,
}
return psh, nil
}
func (psh *psHandle) Exit() {
psh.shell.Exit()
}
type dnsZone map[string]interface{}
func (psh *psHandle) GetDNSServerZoneAll(dnsserver string) ([]string, error) {
stdout, stderr, err := psh.shell.Execute(generatePSZoneAll(dnsserver))
if err != nil {
return nil, err
}
if stderr != "" {
fmt.Printf("STDERROR = %q\n", stderr)
return nil, fmt.Errorf("unexpected stderr from Get-DnsServerZones: %q", stderr)
}
var zones []dnsZone
json.Unmarshal([]byte(stdout), &zones)
var result []string
for _, z := range zones {
zonename := z["ZoneName"].(string)
result = append(result, zonename)
}
return result, nil
}
// powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone.
func generatePSZoneAll(dnsserver string) string {
var b bytes.Buffer
fmt.Fprintf(&b, `Get-DnsServerZone`)
if dnsserver != "" {
fmt.Fprintf(&b, ` -ComputerName "%v"`, dnsserver)
}
fmt.Fprintf(&b, ` | `)
fmt.Fprintf(&b, `ConvertTo-Json`)
return b.String()
}
func (psh *psHandle) GetDNSZoneRecords(dnsserver, domain string) ([]nativeRecord, error) {
tmpfile, err := ioutil.TempFile("", "zonerecords.*.json")
if err != nil {
log.Fatal(err)
}
tmpfile.Close()
stdout, stderr, err := psh.shell.Execute(generatePSZoneDump(dnsserver, domain, tmpfile.Name()))
if err != nil {
return nil, err
}
if stdout != "" {
fmt.Printf("STDOUT = %q\n", stderr)
}
if stderr != "" {
fmt.Printf("STDERROR = %q\n", stderr)
return nil, fmt.Errorf("unexpected stderr from PSZoneDump: %q", stderr)
}
contents, err := utfutil.ReadFile(tmpfile.Name(), utfutil.WINDOWS)
if err != nil {
return nil, err
}
os.Remove(tmpfile.Name())
var records []nativeRecord
json.Unmarshal([]byte(contents), &records)
return records, nil
}
// powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone.
func generatePSZoneDump(dnsserver, domainname string, filename string) string {
var b bytes.Buffer
fmt.Fprintf(&b, `Get-DnsServerResourceRecord`)
if dnsserver != "" {
fmt.Fprintf(&b, ` -ComputerName "%v"`, dnsserver)
}
fmt.Fprintf(&b, ` -ZoneName "%v"`, domainname)
fmt.Fprintf(&b, ` | `)
fmt.Fprintf(&b, `ConvertTo-Json -depth 4`) // Tested with 3 (causes errors). 4 and larger work.
fmt.Fprintf(&b, ` > %s`, filename)
//fmt.Printf("DEBUG PSZoneDump CMD = (\n%s\n)\n", b.String())
return b.String()
}
// Functions for record manipulation
func (psh *psHandle) RecordDelete(dnsserver, domain string, rec *models.RecordConfig) error {
_, stderr, err := psh.shell.Execute(generatePSDelete(dnsserver, domain, rec))
if err != nil {
return err
}
if stderr != "" {
fmt.Printf("STDERROR = %q\n", stderr)
return fmt.Errorf("unexpected stderr from PSDelete: %q", stderr)
}
return nil
}
func generatePSDelete(dnsserver, domain string, rec *models.RecordConfig) string {
var b bytes.Buffer
fmt.Fprintf(&b, `echo DELETE "%s" "%s" "%s"`, rec.Type, rec.Name, rec.GetTargetCombined())
fmt.Fprintf(&b, " ; ")
fmt.Fprintf(&b, `Remove-DnsServerResourceRecord`)
if dnsserver != "" {
fmt.Fprintf(&b, ` -ComputerName "%s"`, dnsserver)
}
fmt.Fprintf(&b, ` -Force`)
fmt.Fprintf(&b, ` -ZoneName "%s"`, domain)
fmt.Fprintf(&b, ` -Name "%s"`, rec.Name)
fmt.Fprintf(&b, ` -RRType "%s"`, rec.Type)
if rec.Type == "MX" {
fmt.Fprintf(&b, ` -RecordData %d,"%s"`, rec.MxPreference, rec.GetTargetField())
} else if rec.Type == "SRV" {
// https://www.gitmemory.com/issue/MicrosoftDocs/windows-powershell-docs/1149/511916884
fmt.Fprintf(&b, ` -RecordData %d,%d,%d,"%s"`, rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.GetTargetField())
} else {
fmt.Fprintf(&b, ` -RecordData "%s"`, rec.GetTargetField())
}
//fmt.Printf("DEBUG PSDelete CMD = (\n%s\n)\n", b.String())
return b.String()
}
func (psh *psHandle) RecordCreate(dnsserver, domain string, rec *models.RecordConfig) error {
_, stderr, err := psh.shell.Execute(generatePSCreate(dnsserver, domain, rec))
if err != nil {
return err
}
if stderr != "" {
fmt.Printf("STDERROR = %q\n", stderr)
return fmt.Errorf("unexpected stderr from PSCreate: %q", stderr)
}
return nil
}
func generatePSCreate(dnsserver, domain string, rec *models.RecordConfig) string {
var b bytes.Buffer
fmt.Fprintf(&b, `echo CREATE "%s" "%s" "%s"`, rec.Type, rec.Name, rec.GetTargetCombined())
fmt.Fprintf(&b, " ; ")
fmt.Fprint(&b, `Add-DnsServerResourceRecord`)
if dnsserver != "" {
fmt.Fprintf(&b, ` -ComputerName "%s"`, dnsserver)
}
fmt.Fprintf(&b, ` -ZoneName "%s"`, domain)
fmt.Fprintf(&b, ` -Name "%s"`, rec.GetLabel())
fmt.Fprintf(&b, ` -TimeToLive $(New-TimeSpan -Seconds %d)`, rec.TTL)
switch rec.Type {
case "A":
fmt.Fprintf(&b, ` -A -IPv4Address "%s"`, rec.GetTargetIP())
case "AAAA":
fmt.Fprintf(&b, ` -AAAA -IPv6Address "%s"`, rec.GetTargetIP())
//case "ATMA":
// fmt.Fprintf(&b, ` -Atma -Address <String> -AddressType {E164 | AESA}`, rec.GetTargetField())
//case "AFSDB":
// fmt.Fprintf(&b, ` -Afsdb -ServerName <String> -SubType <UInt16>`, rec.GetTargetField())
case "SRV":
fmt.Fprintf(&b, ` -Srv -DomainName "%s" -Port %d -Priority %d -Weight %d`, rec.GetTargetField(), rec.SrvPort, rec.SrvPriority, rec.SrvWeight)
case "CNAME":
fmt.Fprintf(&b, ` -CName -HostNameAlias "%s"`, rec.GetTargetField())
//case "X25":
// fmt.Fprintf(&b, ` -X25 -PsdnAddress <String>`, rec.GetTargetField())
//case "WKS":
// fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, rec.GetTargetField())
case "TXT":
fmt.Fprintf(&b, ` -Txt -DescriptiveText "%s"`, rec.GetTargetField())
//case "RT":
// fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, rec.GetTargetField())
//case "RP":
// fmt.Fprintf(&b, ` -RP -Description <String> -ResponsiblePerson <String>`, rec.GetTargetField())
case "PTR":
fmt.Fprintf(&b, ` -Ptr -PtrDomainName "%s"`, rec.GetTargetField())
case "NS":
fmt.Fprintf(&b, ` -NS -NameServer "%s"`, rec.GetTargetField())
case "MX":
fmt.Fprintf(&b, ` -MX -MailExchange "%s" -Preference %d`, rec.GetTargetField(), rec.MxPreference)
//case "ISDN":
// fmt.Fprintf(&b, ` -Isdn -IsdnNumber <String> -IsdnSubAddress <String>`, rec.GetTargetField())
//case "HINFO":
// fmt.Fprintf(&b, ` -HInfo -Cpu <String> -OperatingSystem <String>`, rec.GetTargetField())
//case "DNAME":
// fmt.Fprintf(&b, ` -DName -DomainNameAlias <String>`, rec.GetTargetField())
//case "DHCID":
// fmt.Fprintf(&b, ` -DhcId -DhcpIdentifier <String>`, rec.GetTargetField())
//case "TLSA":
// fmt.Fprintf(&b, ` -TLSA -CertificateAssociationData <System.String> -CertificateUsage {CAConstraint | ServiceCertificateConstraint | TrustAnchorAssertion | DomainIssuedCertificate} -MatchingType {ExactMatch | Sha256Hash | Sha512Hash} -Selector {FullCertificate | SubjectPublicKeyInfo}`, rec.GetTargetField())
default:
panic(fmt.Errorf("generatePSCreate() has not implemented recType=%s recName=%#v content=%#v)",
rec.Type, rec.GetLabel(), rec.GetTargetField()))
// We panic so that we quickly find any switch statements
// that have not been updated for a new RR type.
}
//fmt.Printf("DEBUG PSCreate CMD = (\n%s\n)\n", b.String())
return b.String()
}
func (psh *psHandle) RecordModify(dnsserver, domain string, old, rec *models.RecordConfig) error {
_, stderr, err := psh.shell.Execute(generatePSModify(dnsserver, domain, old, rec))
if err != nil {
return err
}
if stderr != "" {
fmt.Printf("STDERROR = %q\n", stderr)
return fmt.Errorf("unexpected stderr from PSModify: %q", stderr)
}
return nil
}
func generatePSModify(dnsserver, domain string, old, rec *models.RecordConfig) string {
// The simple way is to just remove the old record and insert the new record.
return generatePSDelete(dnsserver, domain, old) + ` ; ` + generatePSCreate(dnsserver, domain, rec)
// NB: SOA records can't be deleted. When we implement them, we'll
// need to special case them and generate an in-place modification
// command.
}
// Note about the old generatePSModify:
//
// The old method is to generate PowerShell code that extracts the resource
// record, clones it, makes modifications to the clone, and replaces the old
// object with the modified clone. In theory this is cleaner.
//
// Sadly that technique is considerably slower (PowerShell seems to take a
// long time doing it) and it is more brittle (each new rType seems to be a
// new adventure).
//
// The other benefit of the Delete/Create method is that it more heavily
// exercises existing code that is known to work.
//
// Sadly I can't bring myself to erase the code yet. I still hope this can
// be fixed. Deep down I know we should just accept that Del/Create is better.
// if old.GetLabel() != rec.GetLabel() {
// panic(fmt.Sprintf("generatePSModify assertion failed: %q != %q", old.GetLabel(), rec.GetLabel()))
// }
//
// var b bytes.Buffer
// fmt.Fprintf(&b, `echo "MODIFY %s %s %s old=(%s) new=(%s):"`, rec.GetLabel(), domain, rec.Type, old.GetTargetCombined(), rec.GetTargetCombined())
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, "$OldObj = Get-DnsServerResourceRecord")
// fmt.Fprintf(&b, ` -ZoneName "%s"`, domain)
// fmt.Fprintf(&b, ` -Name "%s"`, old.GetLabel())
// fmt.Fprintf(&b, ` -RRType "%s"`, old.Type)
// fmt.Fprintf(&b, ` | Where-Object {$_.HostName eq "%s" -and -RRType -eq "%s" -and `, old.GetLabel(), rec.Type)
// switch old.Type {
// case "A":
// fmt.Fprintf(&b, `$_.RecordData.IPv4Address -eq "%s"`, old.GetTargetIP())
// case "AAAA":
// fmt.Fprintf(&b, `$_.RecordData.IPv6Address -eq "%s"`, old.GetTargetIP())
// //case "ATMA":
// // fmt.Fprintf(&b, ` -Atma -Address <String> -AddressType {E164 | AESA}`, old.GetTargetField())
// //case "AFSDB":
// // fmt.Fprintf(&b, ` -Afsdb -ServerName <String> -SubType <UInt16>`, old.GetTargetField())
// case "SRV":
// fmt.Fprintf(&b, `$_.RecordData.DomainName -eq "%s" -and $_.RecordData.Port -eq %d -and $_.RecordData.Priority -eq %d -and $_.RecordData.Weight -eq %d`, old.GetTargetField(), old.SrvPort, old.SrvPriority, old.SrvWeight)
// case "CNAME":
// fmt.Fprintf(&b, `$_.RecordData.HostNameAlias -eq "%s"`, old.GetTargetField())
// //case "X25":
// // fmt.Fprintf(&b, ` -X25 -PsdnAddress <String>`, old.GetTargetField())
// //case "WKS":
// // fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, old.GetTargetField())
// case "TXT":
// fmt.Fprintf(&b, `$_.RecordData.DescriptiveText -eq "%s"`, old.GetTargetField())
// //case "RT":
// // fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, old.GetTargetField())
// //case "RP":
// // fmt.Fprintf(&b, ` -RP -Description <String> -ResponsiblePerson <String>`, old.GetTargetField())
// case "PTR":
// fmt.Fprintf(&b, `$_.RecordData.PtrDomainName -eq "%s"`, old.GetTargetField())
// case "NS":
// fmt.Fprintf(&b, `$_.RecordData.NameServer -eq "%s"`, old.GetTargetField())
// case "MX":
// fmt.Fprintf(&b, `$_.RecordData.MailExchange -eq "%s" -and $_.RecordData.Preference -eq %d`, old.GetTargetField(), old.MxPreference)
// //case "ISDN":
// // fmt.Fprintf(&b, ` -Isdn -IsdnNumber <String> -IsdnSubAddress <String>`, old.GetTargetField())
// //case "HINFO":
// // fmt.Fprintf(&b, ` -HInfo -Cpu <String> -OperatingSystem <String>`, old.GetTargetField())
// //case "DNAME":
// // fmt.Fprintf(&b, ` -DName -DomainNameAlias <String>`, old.GetTargetField())
// //case "DHCID":
// // fmt.Fprintf(&b, ` -DhcId -DhcpIdentifier <String>`, old.GetTargetField())
// //case "TLSA":
// // fmt.Fprintf(&b, ` -TLSA -CertificateAssociationData <System.String> -CertificateUsage {CAConstraint | ServiceCertificateConstraint | TrustAnchorAssertion | DomainIssuedCertificate} -MatchingType {ExactMatch | Sha256Hash | Sha512Hash} -Selector {FullCertificate | SubjectPublicKeyInfo}`, rec.GetTargetField())
// default:
// panic(fmt.Errorf("generatePSModify() has not implemented recType=%q recName=%q content=(%s))",
// rec.Type, rec.GetLabel(), rec.GetTargetCombined()))
// // We panic so that we quickly find any switch statements
// // that have not been updated for a new RR type.
// }
// fmt.Fprintf(&b, "}")
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, `if($OldObj.Length -ne 1){ throw "Error, multiple results for Get-DnsServerResourceRecord" }`)
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, "$NewObj = $OldObj.Clone()")
// fmt.Fprintf(&b, " ; ")
//
// if old.TTL != rec.TTL {
// fmt.Fprintf(&b, `$NewObj.TimeToLive = New-TimeSpan -Seconds %d`, rec.TTL)
// fmt.Fprintf(&b, " ; ")
// }
// switch rec.Type {
// case "A":
// fmt.Fprintf(&b, `$NewObj.RecordData.IPv4Address = "%s"`, rec.GetTargetIP())
// case "AAAA":
// fmt.Fprintf(&b, `$NewObj.RecordData.IPv6Address = "%s"`, rec.GetTargetIP())
// //case "ATMA":
// // fmt.Fprintf(&b, ` -Atma -Address <String> -AddressType {E164 | AESA}`, rec.GetTargetField())
// //case "AFSDB":
// // fmt.Fprintf(&b, ` -Afsdb -ServerName <String> -SubType <UInt16>`, rec.GetTargetField())
// case "SRV":
// fmt.Fprintf(&b, ` -Srv -DomainName "%s" -Port %d -Priority %d -Weight %d`, rec.GetTargetField(), rec.SrvPort, rec.SrvPriority, rec.SrvWeight)
// fmt.Fprintf(&b, `$NewObj.RecordData.DomainName = "%s"`, rec.GetTargetField())
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, `$NewObj.RecordData.Port = %d`, rec.SrvPort)
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, `$NewObj.RecordData.Priority = %d`, rec.SrvPriority)
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, `$NewObj.RecordData.Weight = "%d"`, rec.SrvWeight)
// case "CNAME":
// fmt.Fprintf(&b, `$NewObj.RecordData.HostNameAlias = "%s"`, rec.GetTargetField())
// //case "X25":
// // fmt.Fprintf(&b, ` -X25 -PsdnAddress <String>`, rec.GetTargetField())
// //case "WKS":
// // fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, rec.GetTargetField())
// case "TXT":
// fmt.Fprintf(&b, `$NewObj.RecordData.DescriptiveText = "%s"`, rec.GetTargetField())
// //case "RT":
// // fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, rec.GetTargetField())
// //case "RP":
// // fmt.Fprintf(&b, ` -RP -Description <String> -ResponsiblePerson <String>`, rec.GetTargetField())
// case "PTR":
// fmt.Fprintf(&b, `$NewObj.RecordData.PtrDomainName = "%s"`, rec.GetTargetField())
// case "NS":
// fmt.Fprintf(&b, `$NewObj.RecordData.NameServer = "%s"`, rec.GetTargetField())
// case "MX":
// fmt.Fprintf(&b, `$NewObj.RecordData.MailExchange = "%s"`, rec.GetTargetField())
// fmt.Fprintf(&b, " ; ")
// fmt.Fprintf(&b, `$NewObj.RecordData.Preference = "%d"`, rec.MxPreference)
// //case "ISDN":
// // fmt.Fprintf(&b, ` -Isdn -IsdnNumber <String> -IsdnSubAddress <String>`, rec.GetTargetField())
// //case "HINFO":
// // fmt.Fprintf(&b, ` -HInfo -Cpu <String> -OperatingSystem <String>`, rec.GetTargetField())
// //case "DNAME":
// // fmt.Fprintf(&b, ` -DName -DomainNameAlias <String>`, rec.GetTargetField())
// //case "DHCID":
// // fmt.Fprintf(&b, ` -DhcId -DhcpIdentifier <String>`, rec.GetTargetField())
// //case "TLSA":
// // fmt.Fprintf(&b, ` -TLSA -CertificateAssociationData <System.String> -CertificateUsage {CAConstraint | ServiceCertificateConstraint | TrustAnchorAssertion | DomainIssuedCertificate} -MatchingType {ExactMatch | Sha256Hash | Sha512Hash} -Selector {FullCertificate | SubjectPublicKeyInfo}`, rec.GetTargetField())
// default:
// panic(fmt.Errorf("generatePSModify() update has not implemented recType=%q recName=%q content=(%s))",
// rec.Type, rec.GetLabel(), rec.GetTargetCombined()))
// // We panic so that we quickly find any switch statements
// // that have not been updated for a new RR type.
// }
// fmt.Fprintf(&b, " ; ")
// //fmt.Printf("DEBUG CCMD: %s\n", b.String())
//
// fmt.Fprintf(&b, "Set-DnsServerResourceRecord")
// fmt.Fprintf(&b, ` -ZoneName "%s"`, domain)
// fmt.Fprintf(&b, ` -NewInputObject $NewObj -OldInputObject $OldObj`)
//
// fmt.Printf("DEBUG MCMD: %s", b.String())
// return b.String()

View File

@@ -0,0 +1,169 @@
package msdns
import (
"strings"
"testing"
"github.com/StackExchange/dnscontrol/v3/models"
)
func Test_generatePSZoneAll(t *testing.T) {
type args struct {
dnsserver string
domain string
}
tests := []struct {
name string
args args
want string
}{
{
name: "local",
args: args{},
want: `Get-DnsServerZone | ConvertTo-Json`,
},
{
name: "remote",
args: args{dnsserver: "mydnsserver"},
want: `Get-DnsServerZone -ComputerName "mydnsserver" | ConvertTo-Json`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := generatePSZoneAll(tt.args.dnsserver); got != tt.want {
t.Errorf("generatePSZoneAll() = got=(\n%s\n) want=(\n%s\n)", got, tt.want)
}
})
}
}
func Test_generatePSZoneDump(t *testing.T) {
type args struct {
domainname string
dnsserver string
}
tests := []struct {
name string
args args
want string
}{
{
name: "local",
args: args{domainname: "example.com"},
want: `Get-DnsServerResourceRecord -ZoneName "example.com" | ConvertTo-Json -depth 4 > mytemp.json`,
},
{
name: "remote",
args: args{domainname: "example.com", dnsserver: "mydnsserver"},
want: `Get-DnsServerResourceRecord -ComputerName "mydnsserver" -ZoneName "example.com" | ConvertTo-Json -depth 4 > mytemp.json`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := generatePSZoneDump(tt.args.dnsserver, tt.args.domainname, "mytemp.json"); got != tt.want {
t.Errorf("generatePSZoneDump() = got=(\n%s\n) want=(\n%s\n)", got, tt.want)
}
})
}
}
//func Test_generatePSDelete(t *testing.T) {
// type args struct {
// domain string
// rec *models.RecordConfig
// }
// tests := []struct {
// name string
// args args
// want string
// }{
// // TODO: Add test cases.
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// if got := generatePSDelete(tt.args.domain, tt.args.rec); got != tt.want {
// t.Errorf("generatePSDelete() = %v, want %v", got, tt.want)
// }
// })
// }
//}
// func Test_generatePSCreate(t *testing.T) {
// type args struct {
// domain string
// rec *models.RecordConfig
// }
// tests := []struct {
// name string
// args args
// want string
// }{
// // TODO: Add test cases.
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// if got := generatePSCreate(tt.args.domain, tt.args.rec); got != tt.want {
// t.Errorf("generatePSCreate() = %v, want %v", got, tt.want)
// }
// })
// }
// }
func Test_generatePSModify(t *testing.T) {
recA1 := &models.RecordConfig{
Type: "A",
Name: "@",
Target: "1.2.3.4",
}
recA2 := &models.RecordConfig{
Type: "A",
Name: "@",
Target: "10.20.30.40",
}
recMX1 := &models.RecordConfig{
Type: "MX",
Name: "@",
Target: "foo.com.",
MxPreference: 5,
}
recMX2 := &models.RecordConfig{
Type: "MX",
Name: "@",
Target: "foo2.com.",
MxPreference: 50,
}
type args struct {
domain string
dnsserver string
old *models.RecordConfig
rec *models.RecordConfig
}
tests := []struct {
name string
args args
want string
}{
{name: "A", args: args{domain: "example.com", dnsserver: "", old: recA1, rec: recA2},
want: `echo DELETE "A" "@" "1.2.3.4" ; Remove-DnsServerResourceRecord -Force -ZoneName "example.com" -Name "@" -RRType "A" -RecordData "1.2.3.4" ; echo CREATE "A" "@" "10.20.30.40" ; Add-DnsServerResourceRecord -ZoneName "example.com" -Name "@" -TimeToLive $(New-TimeSpan -Seconds 0) -A -IPv4Address "10.20.30.40"`,
},
{name: "MX1", args: args{domain: "example.com", dnsserver: "", old: recMX1, rec: recMX2},
want: `echo DELETE "MX" "@" "5 foo.com." ; Remove-DnsServerResourceRecord -Force -ZoneName "example.com" -Name "@" -RRType "MX" -RecordData 5,"foo.com." ; echo CREATE "MX" "@" "50 foo2.com." ; Add-DnsServerResourceRecord -ZoneName "example.com" -Name "@" -TimeToLive $(New-TimeSpan -Seconds 0) -MX -MailExchange "foo2.com." -Preference 50`,
},
{name: "A-remote", args: args{domain: "example.com", dnsserver: "myremote", old: recA1, rec: recA2},
want: `echo DELETE "A" "@" "1.2.3.4" ; Remove-DnsServerResourceRecord -ComputerName "myremote" -Force -ZoneName "example.com" -Name "@" -RRType "A" -RecordData "1.2.3.4" ; echo CREATE "A" "@" "10.20.30.40" ; Add-DnsServerResourceRecord -ComputerName "myremote" -ZoneName "example.com" -Name "@" -TimeToLive $(New-TimeSpan -Seconds 0) -A -IPv4Address "10.20.30.40"`,
},
{name: "MX1-remote", args: args{domain: "example.com", dnsserver: "yourremote", old: recMX1, rec: recMX2},
want: `echo DELETE "MX" "@" "5 foo.com." ; Remove-DnsServerResourceRecord -ComputerName "yourremote" -Force -ZoneName "example.com" -Name "@" -RRType "MX" -RecordData 5,"foo.com." ; echo CREATE "MX" "@" "50 foo2.com." ; Add-DnsServerResourceRecord -ComputerName "yourremote" -ZoneName "example.com" -Name "@" -TimeToLive $(New-TimeSpan -Seconds 0) -MX -MailExchange "foo2.com." -Preference 50`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := generatePSModify(tt.args.dnsserver, tt.args.domain, tt.args.old, tt.args.rec); strings.TrimSpace(got) != strings.TrimSpace(tt.want) {
t.Errorf("generatePSModify() = got=(\n%s\n) want=(\n%s\n)", got, tt.want)
}
})
}
}

48
providers/msdns/types.go Normal file
View File

@@ -0,0 +1,48 @@
package msdns
import (
"encoding/json"
"github.com/StackExchange/dnscontrol/v3/models"
)
// DNSAccessor describes a system that can access Microsoft DNS.
type DNSAccessor interface {
Exit()
GetDNSServerZoneAll(dnsserver string) ([]string, error)
GetDNSZoneRecords(dnsserver, domain string) ([]nativeRecord, error)
RecordCreate(dnsserver, domain string, rec *models.RecordConfig) error
RecordDelete(dnsserver, domain string, rec *models.RecordConfig) error
RecordModify(dnsserver, domain string, old, rec *models.RecordConfig) error
}
// nativeRecord the JSON received from PowerShell when listing all DNS
// records in a zone.
type nativeRecord struct {
//CimClass interface{} `json:"CimClass"`
//CimInstanceProperties interface{} `json:"CimInstanceProperties"`
//CimSystemProperties interface{} `json:"CimSystemProperties"`
//DistinguishedName string `json:"DistinguishedName"`
//RecordClass string `json:"RecordClass"`
RecordType string `json:"RecordType"`
HostName string `json:"HostName"`
RecordData struct {
CimInstanceProperties []ciProperty `json:"CimInstanceProperties"`
} `json:"RecordData"`
TimeToLive struct {
TotalSeconds float64 `json:"TotalSeconds"`
} `json:"TimeToLive"`
}
type ciProperty struct {
Name string `json:"Name"`
Value json.RawMessage `json:"Value,omitempty"`
}
type ciValueDuration struct {
TotalSeconds float64 `json:"TotalSeconds"`
}
// NB(tlim): The above structs were created using the help of:
// Get-DnsServerResourceRecord -ZoneName example.com | where { $_.RecordType -eq "SOA" } | select $_.RecordData | ConvertTo-Json -depth 10
// and pass it to https://mholt.github.io/json-to-go/