mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 06:46:19 +08:00
Improve MSDNS naptr support (#1165)
* MSDNS: Improve reliability of zone dump * Update tests * MSDNS: Add initial NAPTR support * Update * fix tests * fix tests * Fixing integration tests for NAPTR * Handle bad JSON. Handle NAPTR TTLs
This commit is contained in:
parent
6d64fc8cac
commit
654736be29
|
@ -1,6 +1,7 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -249,6 +250,16 @@ func GetZone(args GetZoneArgs) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonQuoted returns a properly escaped JSON string (without quotes).
|
||||||
|
func jsonQuoted(i string) string {
|
||||||
|
// https://stackoverflow.com/questions/51691901
|
||||||
|
b, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) string {
|
func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) string {
|
||||||
|
|
||||||
target := rec.GetTargetCombined()
|
target := rec.GetTargetCombined()
|
||||||
|
@ -272,6 +283,15 @@ func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) str
|
||||||
return makeCaa(rec, ttlop)
|
return makeCaa(rec, ttlop)
|
||||||
case "MX":
|
case "MX":
|
||||||
target = fmt.Sprintf("%d, '%s'", rec.MxPreference, rec.GetTargetField())
|
target = fmt.Sprintf("%d, '%s'", rec.MxPreference, rec.GetTargetField())
|
||||||
|
case "NAPTR":
|
||||||
|
target = fmt.Sprintf(`%d, %d, %s, %s, %s, %s`,
|
||||||
|
rec.NaptrOrder, // 1
|
||||||
|
rec.NaptrPreference, // 10
|
||||||
|
jsonQuoted(rec.NaptrFlags), // U
|
||||||
|
jsonQuoted(rec.NaptrService), // E2U+sip
|
||||||
|
jsonQuoted(rec.NaptrRegexp), // regex
|
||||||
|
jsonQuoted(rec.GetTargetField()), // .
|
||||||
|
)
|
||||||
case "SSHFP":
|
case "SSHFP":
|
||||||
target = fmt.Sprintf("%d, %d, '%s'", rec.SshfpAlgorithm, rec.SshfpFingerprint, rec.GetTargetField())
|
target = fmt.Sprintf("%d, %d, '%s'", rec.SshfpAlgorithm, rec.SshfpFingerprint, rec.GetTargetField())
|
||||||
case "SOA":
|
case "SOA":
|
||||||
|
|
|
@ -218,7 +218,7 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
|
||||||
if len(corrections) != 0 {
|
if len(corrections) != 0 {
|
||||||
t.Logf("Expected 0 corrections on second run, but found %d.", len(corrections))
|
t.Logf("Expected 0 corrections on second run, but found %d.", len(corrections))
|
||||||
for i, c := range corrections {
|
for i, c := range corrections {
|
||||||
t.Logf("#%d: %s", i, c.Msg)
|
t.Logf("UNEXPECTED #%d: %s", i, c.Msg)
|
||||||
}
|
}
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ import (
|
||||||
|
|
||||||
// SetTargetNAPTR sets the NAPTR fields.
|
// SetTargetNAPTR sets the NAPTR fields.
|
||||||
func (rc *RecordConfig) SetTargetNAPTR(order uint16, preference uint16, flags string, service string, regexp string, target string) error {
|
func (rc *RecordConfig) SetTargetNAPTR(order uint16, preference uint16, flags string, service string, regexp string, target string) error {
|
||||||
|
if target == "" {
|
||||||
|
target = "."
|
||||||
|
}
|
||||||
rc.NaptrOrder = order
|
rc.NaptrOrder = order
|
||||||
rc.NaptrPreference = preference
|
rc.NaptrPreference = preference
|
||||||
rc.NaptrFlags = flags
|
rc.NaptrFlags = flags
|
||||||
|
|
|
@ -55,14 +55,16 @@ func (rc *RecordConfig) GetTargetCombined() string {
|
||||||
case "AZURE_ALIAS":
|
case "AZURE_ALIAS":
|
||||||
// Differentiate between multiple AZURE_ALIASs on the same label.
|
// Differentiate between multiple AZURE_ALIASs on the same label.
|
||||||
return fmt.Sprintf("%s atype=%s", rc.target, rc.AzureAlias["type"])
|
return fmt.Sprintf("%s atype=%s", rc.target, rc.AzureAlias["type"])
|
||||||
case "SOA":
|
|
||||||
return fmt.Sprintf("%s %v %d %d %d %d %d", rc.target, rc.SoaMbox, rc.SoaSerial, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl)
|
|
||||||
default:
|
default:
|
||||||
// Just return the target.
|
// Just return the target.
|
||||||
return rc.target
|
return rc.target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rc.Type == "SOA" {
|
||||||
|
return fmt.Sprintf("%s %v %d %d %d %d %d", rc.target, rc.SoaMbox, rc.SoaSerial, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl)
|
||||||
|
}
|
||||||
|
|
||||||
return rc.zoneFileQuoted()
|
return rc.zoneFileQuoted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +75,9 @@ func (rc *RecordConfig) zoneFileQuoted() string {
|
||||||
// Sadly String() always includes a header, which we must strip out.
|
// Sadly String() always includes a header, which we must strip out.
|
||||||
// TODO(tlim): Request the dns project add a function that returns
|
// TODO(tlim): Request the dns project add a function that returns
|
||||||
// the string without the header.
|
// the string without the header.
|
||||||
|
if rc.Type == "NAPTR" && rc.GetTargetField() == "" {
|
||||||
|
rc.SetTarget(".")
|
||||||
|
}
|
||||||
rr := rc.ToRR()
|
rr := rc.ToRR()
|
||||||
header := rr.Header().String()
|
header := rr.Header().String()
|
||||||
full := rr.String()
|
full := rr.String()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
D("foo.com","none",
|
D("foo.com","none",
|
||||||
NAPTR("@",100,10,"U","E2U+sip","!^.*$!sip:customer-service@example.com!","example"),
|
NAPTR("@",100,10,"U","E2U+sip","!^.*$!sip:customer-service@example.com!","example"),
|
||||||
NAPTR("@",102,10,"U","E2U+email","!^.*$!mailto:information@example.com!","example")
|
NAPTR("@",102,10,"U","E2U+email","!^.*$!mailto:information@example.com!","example"),
|
||||||
|
NAPTR("@",103,10,"U","E2U+email","!^.*$!mailto:information@example.com!",""),
|
||||||
|
NAPTR("@",104,10,"U","E2U+email","!^.*$!mailto:information@example.com!",".")
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,33 +1,53 @@
|
||||||
{
|
{
|
||||||
"registrars": [],
|
|
||||||
"dns_providers": [],
|
"dns_providers": [],
|
||||||
"domains": [
|
"domains": [
|
||||||
{
|
{
|
||||||
"name": "foo.com",
|
|
||||||
"registrar": "none",
|
|
||||||
"dnsProviders": {},
|
"dnsProviders": {},
|
||||||
|
"name": "foo.com",
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
"type": "NAPTR",
|
|
||||||
"name": "@",
|
"name": "@",
|
||||||
"target": "example",
|
"naptrflags": "U",
|
||||||
"naptrorder": 100,
|
"naptrorder": 100,
|
||||||
"naptrpreference": 10,
|
"naptrpreference": 10,
|
||||||
"naptrflags": "U",
|
"naptrregexp": "!^.*$!sip:customer-service@example.com!",
|
||||||
"naptrservice": "E2U+sip",
|
"naptrservice": "E2U+sip",
|
||||||
"naptrregexp": "!^.*$!sip:customer-service@example.com!"
|
"target": "example",
|
||||||
|
"type": "NAPTR"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "NAPTR",
|
|
||||||
"name": "@",
|
"name": "@",
|
||||||
"target": "example",
|
"naptrflags": "U",
|
||||||
"naptrorder": 102,
|
"naptrorder": 102,
|
||||||
"naptrpreference": 10,
|
"naptrpreference": 10,
|
||||||
"naptrflags": "U",
|
"naptrregexp": "!^.*$!mailto:information@example.com!",
|
||||||
"naptrservice": "E2U+email",
|
"naptrservice": "E2U+email",
|
||||||
"naptrregexp": "!^.*$!mailto:information@example.com!"
|
"target": "example",
|
||||||
|
"type": "NAPTR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@",
|
||||||
|
"naptrflags": "U",
|
||||||
|
"naptrorder": 103,
|
||||||
|
"naptrpreference": 10,
|
||||||
|
"naptrregexp": "!^.*$!mailto:information@example.com!",
|
||||||
|
"naptrservice": "E2U+email",
|
||||||
|
"target": "",
|
||||||
|
"type": "NAPTR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@",
|
||||||
|
"naptrflags": "U",
|
||||||
|
"naptrorder": 104,
|
||||||
|
"naptrpreference": 10,
|
||||||
|
"naptrregexp": "!^.*$!mailto:information@example.com!",
|
||||||
|
"naptrservice": "E2U+email",
|
||||||
|
"target": ".",
|
||||||
|
"type": "NAPTR"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"registrar": "none"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"registrars": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
$TTL 300
|
$TTL 300
|
||||||
@ IN NAPTR 100 10 "U" "E2U+sip" "!^.*$!sip:customer-service@example.com!" example.foo.com.
|
@ IN NAPTR 100 10 "U" "E2U+sip" "!^.*$!sip:customer-service@example.com!" example
|
||||||
IN NAPTR 102 10 "U" "E2U+email" "!^.*$!mailto:information@example.com!" example.foo.com.
|
IN NAPTR 102 10 "U" "E2U+email" "!^.*$!mailto:information@example.com!" example
|
||||||
|
IN NAPTR 103 10 "U" "E2U+email" "!^.*$!mailto:information@example.com!" .
|
||||||
|
IN NAPTR 104 10 "U" "E2U+email" "!^.*$!mailto:information@example.com!" .
|
||||||
|
|
|
@ -180,7 +180,9 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
|
||||||
case "PTR":
|
case "PTR":
|
||||||
check(checkTarget(target))
|
check(checkTarget(target))
|
||||||
case "NAPTR":
|
case "NAPTR":
|
||||||
check(checkTarget(target))
|
if target != "" {
|
||||||
|
check(checkTarget(target))
|
||||||
|
}
|
||||||
case "ALIAS":
|
case "ALIAS":
|
||||||
check(checkTarget(target))
|
check(checkTarget(target))
|
||||||
case "SOA":
|
case "SOA":
|
||||||
|
@ -345,7 +347,7 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonicalize Targets.
|
// Canonicalize Targets.
|
||||||
if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NAPTR" || rec.Type == "NS" || rec.Type == "SRV" {
|
if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NS" || rec.Type == "SRV" {
|
||||||
// #rtype_variations
|
// #rtype_variations
|
||||||
// These record types have a target that is a hostname.
|
// These record types have a target that is a hostname.
|
||||||
// We normalize them to a FQDN so there is less variation to handle. If a
|
// We normalize them to a FQDN so there is less variation to handle. If a
|
||||||
|
|
|
@ -90,6 +90,9 @@ func nativeToRecords(nr nativeRecord, origin string) (*models.RecordConfig, erro
|
||||||
rc.SetTargetMX(uint16(uprops["Preference"]), sprops["MailExchange"])
|
rc.SetTargetMX(uint16(uprops["Preference"]), sprops["MailExchange"])
|
||||||
case "NS":
|
case "NS":
|
||||||
rc.SetTarget(sprops["NameServer"])
|
rc.SetTarget(sprops["NameServer"])
|
||||||
|
case "NAPTR":
|
||||||
|
n := decodeRecordDataNaptr(sprops["Data"])
|
||||||
|
rc.SetTargetNAPTR(n.NaptrOrder, n.NaptrPreference, n.NaptrFlags, n.NaptrService, n.NaptrRegexp, n.GetTargetField())
|
||||||
case "PTR":
|
case "PTR":
|
||||||
rc.SetTarget(sprops["PtrDomainName"])
|
rc.SetTarget(sprops["PtrDomainName"])
|
||||||
case "SRV":
|
case "SRV":
|
||||||
|
|
7
providers/msdns/escape.go
Normal file
7
providers/msdns/escape.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package msdns
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func escapePS(s string) string {
|
||||||
|
return `'` + strings.Replace(s, `'`, `"`, -1) + `'`
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ var features = providers.DocumentationNotes{
|
||||||
providers.CanUseAlias: providers.Cannot(),
|
providers.CanUseAlias: providers.Cannot(),
|
||||||
providers.CanUseCAA: providers.Cannot(),
|
providers.CanUseCAA: providers.Cannot(),
|
||||||
providers.CanUseDS: providers.Unimplemented(),
|
providers.CanUseDS: providers.Unimplemented(),
|
||||||
|
providers.CanUseNAPTR: providers.Can(),
|
||||||
providers.CanUsePTR: providers.Can(),
|
providers.CanUsePTR: providers.Can(),
|
||||||
providers.CanUseSRV: providers.Can(),
|
providers.CanUseSRV: providers.Can(),
|
||||||
providers.CanUseTLSA: providers.Unimplemented(),
|
providers.CanUseTLSA: providers.Unimplemented(),
|
||||||
|
|
110
providers/msdns/naptr.go
Normal file
110
providers/msdns/naptr.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package msdns
|
||||||
|
|
||||||
|
// NAPTR records are not supported by the PowerShell module.
|
||||||
|
// Until this bug is fixed we use old-school commands instead.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generatePSCreateNaptr(dnsServerName, domain string, rec *models.RecordConfig) string {
|
||||||
|
|
||||||
|
var computername string
|
||||||
|
if dnsServerName != "" {
|
||||||
|
computername = escapePS(dnsServerName) + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
fmt.Fprintf(&b, `$zoneName = %s ; `, escapePS(domain))
|
||||||
|
fmt.Fprintf(&b, `$rrName = %s ; `, escapePS(rec.Name))
|
||||||
|
fmt.Fprintf(&b, `$Order = %d ; `, rec.NaptrOrder)
|
||||||
|
fmt.Fprintf(&b, `$Preference = %d ; `, rec.NaptrPreference)
|
||||||
|
fmt.Fprintf(&b, `$Flags = %s ; `, escapePS(rec.NaptrFlags))
|
||||||
|
fmt.Fprintf(&b, `$Service = %s ; `, escapePS(rec.NaptrService))
|
||||||
|
fmt.Fprintf(&b, `$Regex = %s ; `, escapePS(rec.NaptrRegexp))
|
||||||
|
fmt.Fprintf(&b, `$Replacement = %s ; `, escapePS(rec.GetTargetField()))
|
||||||
|
fmt.Fprintf(&b, `dnscmd %s/recordadd $zoneName $rrName %d naptr $Order $Preference $Flags $Service $Regex $Replacement ; `, computername, rec.TTL)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePSDeleteNaptr(dnsServerName, domain string, rec *models.RecordConfig) string {
|
||||||
|
target := rec.GetTargetField()
|
||||||
|
if target == "" {
|
||||||
|
target = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
var computername string
|
||||||
|
if dnsServerName != "" {
|
||||||
|
computername = escapePS(dnsServerName) + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
fmt.Fprintf(&b, `$zoneName = %s ; `, escapePS(domain))
|
||||||
|
fmt.Fprintf(&b, `$rrName = %s ; `, escapePS(rec.Name))
|
||||||
|
fmt.Fprintf(&b, `$Order = %d ; `, rec.NaptrOrder)
|
||||||
|
fmt.Fprintf(&b, `$Preference = %d ; `, rec.NaptrPreference)
|
||||||
|
fmt.Fprintf(&b, `$Flags = %s ; `, escapePS(rec.NaptrFlags))
|
||||||
|
fmt.Fprintf(&b, `$Service = %s ; `, escapePS(rec.NaptrService))
|
||||||
|
fmt.Fprintf(&b, `$Regex = %s ; `, escapePS(rec.NaptrRegexp))
|
||||||
|
fmt.Fprintf(&b, `$Replacement = %s ; `, escapePS(target))
|
||||||
|
fmt.Fprintf(&b, `dnscmd %s/recorddelete $zoneName $rrName naptr $Order $Preference $Flags $Service $Regex $Replacement /f ; `, computername)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// decoding
|
||||||
|
|
||||||
|
func decodeRecordDataNaptr(s string) models.RecordConfig {
|
||||||
|
// These strings look like this:
|
||||||
|
// C8AFB0B30153075349502B4432540474657374165F7369702E5F7463702E6578616D706C652E6F72672E
|
||||||
|
// The first 2 groups of 16 bits (4 hex digits) are uinet16.
|
||||||
|
// The rest are 4 length-prefixed strings.
|
||||||
|
// The string should be entirely consumed.
|
||||||
|
rc := models.RecordConfig{}
|
||||||
|
|
||||||
|
s, rc.NaptrOrder = eatUint16(s)
|
||||||
|
s, rc.NaptrPreference = eatUint16(s)
|
||||||
|
s, rc.NaptrFlags = eatString(s)
|
||||||
|
s, rc.NaptrService = eatString(s)
|
||||||
|
s, rc.NaptrRegexp = eatString(s)
|
||||||
|
s, targ := eatString(s)
|
||||||
|
rc.SetTarget(targ)
|
||||||
|
|
||||||
|
// At this point we should have consumed the entire string.
|
||||||
|
if s != "" {
|
||||||
|
fmt.Printf("WARNING: REMAINDER:=%q\n", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// eatUint16 consumes the first 16 bits of the string, returns it as a
|
||||||
|
// uint16, and returns the remaining bytes of the string.
|
||||||
|
func eatUint16(s string) (string, uint16) {
|
||||||
|
value, err := strconv.ParseUint(s[2:4]+s[0:2], 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return s[4:], uint16(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eatString consumes an encoded string (8-bit length byte, then the string).
|
||||||
|
func eatString(s string) (string, string) {
|
||||||
|
sl, err := strconv.ParseUint(s[:2], 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
last := 2 + sl*2
|
||||||
|
hexcoded := s[2:last]
|
||||||
|
ret, err := hex.DecodeString(hexcoded)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[last:], string(ret)
|
||||||
|
}
|
30
providers/msdns/naptr_test.go
Normal file
30
providers/msdns/naptr_test.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package msdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_decodeRecordDataNaptr(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
s string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want models.RecordConfig
|
||||||
|
}{
|
||||||
|
{"01", args{"C8AFB0B30153075349502B4432540474657374165F7369702E5F7463702E6578616D706C652E6F72672E"}, models.RecordConfig{NaptrOrder: 45000, NaptrPreference: 46000, NaptrFlags: "S", NaptrService: "SIP+D2T", NaptrRegexp: "test", Name: "_sip._tcp.example.org."}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt.want.SetTarget(tt.want.Name)
|
||||||
|
tt.want.Name = ""
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := decodeRecordDataNaptr(tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("decodeRecordDataNaptr() = %+v, want %+v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,7 +73,10 @@ func (psh *psHandle) GetDNSServerZoneAll(dnsserver string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var zones []dnsZone
|
var zones []dnsZone
|
||||||
json.Unmarshal([]byte(stdout), &zones)
|
err = json.Unmarshal([]byte(stdout), &zones)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
for _, z := range zones {
|
for _, z := range zones {
|
||||||
|
@ -110,11 +113,11 @@ func (psh *psHandle) GetDNSZoneRecords(dnsserver, domain string) ([]nativeRecord
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if stderr != "" {
|
if stderr != "" {
|
||||||
fmt.Printf("STDERROR = %q\n", stderr)
|
fmt.Printf("STDERROR GetDNSZR = %q\n", stderr)
|
||||||
return nil, fmt.Errorf("unexpected stderr from PSZoneDump: %q", stderr)
|
return nil, fmt.Errorf("unexpected stderr from PSZoneDump: %q", stderr)
|
||||||
}
|
}
|
||||||
if stdout != "" {
|
if stdout != "" {
|
||||||
fmt.Printf("STDOUT = %q\n", stdout)
|
fmt.Printf("STDOUT GetDNSZR = %q\n", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
contents, err := utfutil.ReadFile(filename, utfutil.UTF8)
|
contents, err := utfutil.ReadFile(filename, utfutil.UTF8)
|
||||||
|
@ -123,10 +126,21 @@ func (psh *psHandle) GetDNSZoneRecords(dnsserver, domain string) ([]nativeRecord
|
||||||
}
|
}
|
||||||
os.Remove(filename) // TODO(tlim): There should be a debug flag that leaves the tmp file around.
|
os.Remove(filename) // TODO(tlim): There should be a debug flag that leaves the tmp file around.
|
||||||
|
|
||||||
|
//fmt.Printf("CONTENTS = %s\n", contents)
|
||||||
|
//fmt.Printf("CONTENTS STR = %q\n", contents[:10])
|
||||||
|
//fmt.Printf("CONTENTS HEX = %v\n", []byte(contents)[:10])
|
||||||
|
//ioutil.WriteFile("/temp/list.json", contents, 0777)
|
||||||
var records []nativeRecord
|
var records []nativeRecord
|
||||||
err = json.Unmarshal(contents, &records)
|
err = json.Unmarshal(contents, &records)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("PSZoneDump json error: %w", err)
|
// PowerShell generates bad JSON if there is only one record. Therefore, if there
|
||||||
|
// is an error we try decoding the bad format before completing erroring out.
|
||||||
|
// The "bad JSON" is that they generate a single record instead of a list of length 1.
|
||||||
|
records = append(records, nativeRecord{})
|
||||||
|
err2 := json.Unmarshal(contents, &(records[0]))
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, fmt.Errorf("PSZoneDump json error: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return records, nil
|
return records, nil
|
||||||
|
@ -141,17 +155,35 @@ func generatePSZoneDump(dnsserver, domainname, filename string) string {
|
||||||
// a BOM. A UTF-8 file shouldn't have a BOM, but Microsoft messed up.
|
// a BOM. A UTF-8 file shouldn't have a BOM, but Microsoft messed up.
|
||||||
// When we switch to PowerShell Core, the BOM will disappear.
|
// When we switch to PowerShell Core, the BOM will disappear.
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
// Set the output to be UTF8. Previously we didn't do that and the
|
||||||
|
// output was twice as large, plus it required an extra conversion
|
||||||
|
// step. Windows PowerShell is native UTF16 but PowerShell Core is
|
||||||
|
// native UTF8, thus this may not be needed if we move to Core.
|
||||||
fmt.Fprintf(&b, `$OutputEncoding = [Text.UTF8Encoding]::UTF8 ; `)
|
fmt.Fprintf(&b, `$OutputEncoding = [Text.UTF8Encoding]::UTF8 ; `)
|
||||||
|
|
||||||
|
// Output everything we know about the zone.
|
||||||
fmt.Fprintf(&b, `Get-DnsServerResourceRecord`)
|
fmt.Fprintf(&b, `Get-DnsServerResourceRecord`)
|
||||||
if dnsserver != "" {
|
if dnsserver != "" {
|
||||||
fmt.Fprintf(&b, ` -ComputerName "%v"`, dnsserver)
|
fmt.Fprintf(&b, ` -ComputerName "%v"`, dnsserver)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, ` -ZoneName "%v"`, domainname)
|
fmt.Fprintf(&b, ` -ZoneName "%v"`, domainname)
|
||||||
|
|
||||||
|
// Strip out the `Cim*` properties at the root. This shrinks one
|
||||||
|
// zone from 99M to 11M. We don't need the Cim* properties (at
|
||||||
|
// least the ones at the root) and decocding 99M of JSON was slow
|
||||||
|
// (30+ minutes).
|
||||||
|
// NB(tlim): Windows PowerShell requires the `-Property *` but
|
||||||
|
// Windows PowerShell Core makes that optional.
|
||||||
fmt.Fprintf(&b, ` | `)
|
fmt.Fprintf(&b, ` | `)
|
||||||
fmt.Fprintf(&b, `Select-Object -Property * -ExcludeProperty Cim*`)
|
fmt.Fprintf(&b, `Select-Object -Property * -ExcludeProperty Cim*`)
|
||||||
fmt.Fprintf(&b, ` | `)
|
fmt.Fprintf(&b, ` | `)
|
||||||
fmt.Fprintf(&b, `ConvertTo-Json -depth 4`) // Tested with 3 (causes errors). 4 and larger work.
|
fmt.Fprintf(&b, `ConvertTo-Json -depth 4`) // Tested with 3 (causes errors). 4 and larger work.
|
||||||
fmt.Fprintf(&b, ` | `)
|
fmt.Fprintf(&b, ` | `)
|
||||||
|
|
||||||
|
// Prevously we captured stdout. Now we write it to a file. This is
|
||||||
|
// safer since there is no chance of junk accidentally being mixed
|
||||||
|
// into stdout.
|
||||||
fmt.Fprintf(&b, `Out-File "%s" -Encoding utf8`, filename)
|
fmt.Fprintf(&b, `Out-File "%s" -Encoding utf8`, filename)
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
@ -159,7 +191,16 @@ func generatePSZoneDump(dnsserver, domainname, filename string) string {
|
||||||
// Functions for record manipulation
|
// Functions for record manipulation
|
||||||
|
|
||||||
func (psh *psHandle) RecordDelete(dnsserver, domain string, rec *models.RecordConfig) error {
|
func (psh *psHandle) RecordDelete(dnsserver, domain string, rec *models.RecordConfig) error {
|
||||||
_, stderr, err := psh.shell.Execute(generatePSDelete(dnsserver, domain, rec))
|
|
||||||
|
var c string
|
||||||
|
if rec.Type == "NAPTR" {
|
||||||
|
c = generatePSDeleteNaptr(dnsserver, domain, rec)
|
||||||
|
//fmt.Printf("DEBUG: deleteNAPTR: %s\n", c)
|
||||||
|
} else {
|
||||||
|
c = generatePSDelete(dnsserver, domain, rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, stderr, err := psh.shell.Execute(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -171,9 +212,17 @@ func (psh *psHandle) RecordDelete(dnsserver, domain string, rec *models.RecordCo
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatePSDelete(dnsserver, domain string, rec *models.RecordConfig) string {
|
func generatePSDelete(dnsserver, domain string, rec *models.RecordConfig) string {
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
fmt.Fprintf(&b, `echo DELETE "%s" "%s" "%s"`, rec.Type, rec.Name, rec.GetTargetCombined())
|
fmt.Fprintf(&b, `echo DELETE "%s" "%s" "..."`, rec.Type, rec.Name)
|
||||||
fmt.Fprintf(&b, " ; ")
|
fmt.Fprintf(&b, " ; ")
|
||||||
|
|
||||||
|
if rec.Type == "NAPTR" {
|
||||||
|
x := b.String() + generatePSDeleteNaptr(dnsserver, domain, rec)
|
||||||
|
//fmt.Printf("NAPTR DELETE: %s\n", x)
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&b, `Remove-DnsServerResourceRecord`)
|
fmt.Fprintf(&b, `Remove-DnsServerResourceRecord`)
|
||||||
if dnsserver != "" {
|
if dnsserver != "" {
|
||||||
fmt.Fprintf(&b, ` -ComputerName "%s"`, dnsserver)
|
fmt.Fprintf(&b, ` -ComputerName "%s"`, dnsserver)
|
||||||
|
@ -197,12 +246,23 @@ func generatePSDelete(dnsserver, domain string, rec *models.RecordConfig) string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (psh *psHandle) RecordCreate(dnsserver, domain string, rec *models.RecordConfig) error {
|
func (psh *psHandle) RecordCreate(dnsserver, domain string, rec *models.RecordConfig) error {
|
||||||
_, stderr, err := psh.shell.Execute(generatePSCreate(dnsserver, domain, rec))
|
|
||||||
|
var c string
|
||||||
|
if rec.Type == "NAPTR" {
|
||||||
|
c = generatePSCreateNaptr(dnsserver, domain, rec)
|
||||||
|
//fmt.Printf("DEBUG: createNAPTR: %s\n", c)
|
||||||
|
} else {
|
||||||
|
c = generatePSCreate(dnsserver, domain, rec)
|
||||||
|
//fmt.Printf("DEBUG: PScreate\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, stderr, err := psh.shell.Execute(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if stderr != "" {
|
if stderr != "" {
|
||||||
fmt.Printf("STDERROR = %q\n", stderr)
|
fmt.Printf("STDOUT RecordCreate = %s\n", stdout)
|
||||||
|
fmt.Printf("STDERROR RecordCreate = %q\n", stderr)
|
||||||
return fmt.Errorf("unexpected stderr from PSCreate: %q", stderr)
|
return fmt.Errorf("unexpected stderr from PSCreate: %q", stderr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -210,9 +270,13 @@ func (psh *psHandle) RecordCreate(dnsserver, domain string, rec *models.RecordCo
|
||||||
|
|
||||||
func generatePSCreate(dnsserver, domain string, rec *models.RecordConfig) string {
|
func generatePSCreate(dnsserver, domain string, rec *models.RecordConfig) string {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
fmt.Fprintf(&b, `echo CREATE "%s" "%s" "%s"`, rec.Type, rec.Name, rec.GetTargetCombined())
|
fmt.Fprintf(&b, `echo CREATE "%s" "%s" "..."`, rec.Type, rec.Name)
|
||||||
fmt.Fprintf(&b, " ; ")
|
fmt.Fprintf(&b, " ; ")
|
||||||
|
|
||||||
|
if rec.Type == "NAPTR" {
|
||||||
|
return b.String() + generatePSCreateNaptr(dnsserver, domain, rec)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprint(&b, `Add-DnsServerResourceRecord`)
|
fmt.Fprint(&b, `Add-DnsServerResourceRecord`)
|
||||||
if dnsserver != "" {
|
if dnsserver != "" {
|
||||||
fmt.Fprintf(&b, ` -ComputerName "%s"`, dnsserver)
|
fmt.Fprintf(&b, ` -ComputerName "%s"`, dnsserver)
|
||||||
|
@ -238,8 +302,8 @@ func generatePSCreate(dnsserver, domain string, rec *models.RecordConfig) string
|
||||||
//case "WKS":
|
//case "WKS":
|
||||||
// fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, rec.GetTargetField())
|
// fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, rec.GetTargetField())
|
||||||
case "TXT":
|
case "TXT":
|
||||||
fmt.Printf("DEBUG TXT len = %v\n", rec.TxtStrings)
|
//fmt.Printf("DEBUG TXT len = %v\n", rec.TxtStrings)
|
||||||
fmt.Printf("DEBUG TXT target = %q\n", rec.GetTargetField())
|
//fmt.Printf("DEBUG TXT target = %q\n", rec.GetTargetField())
|
||||||
fmt.Fprintf(&b, ` -Txt -DescriptiveText %s`, rec.GetTargetField())
|
fmt.Fprintf(&b, ` -Txt -DescriptiveText %s`, rec.GetTargetField())
|
||||||
//case "RT":
|
//case "RT":
|
||||||
// fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, rec.GetTargetField())
|
// fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, rec.GetTargetField())
|
||||||
|
|
|
@ -146,16 +146,16 @@ func Test_generatePSModify(t *testing.T) {
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{name: "A", args: args{domain: "example.com", dnsserver: "", old: recA1, rec: recA2},
|
{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"`,
|
want: `echo DELETE "A" "@" "..." ; Remove-DnsServerResourceRecord -Force -ZoneName "example.com" -Name "@" -RRType "A" -RecordData "1.2.3.4" ; echo CREATE "A" "@" "..." ; 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},
|
{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`,
|
want: `echo DELETE "MX" "@" "..." ; Remove-DnsServerResourceRecord -Force -ZoneName "example.com" -Name "@" -RRType "MX" -RecordData 5,"foo.com." ; echo CREATE "MX" "@" "..." ; 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},
|
{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"`,
|
want: `echo DELETE "A" "@" "..." ; Remove-DnsServerResourceRecord -ComputerName "myremote" -Force -ZoneName "example.com" -Name "@" -RRType "A" -RecordData "1.2.3.4" ; echo CREATE "A" "@" "..." ; 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},
|
{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`,
|
want: `echo DELETE "MX" "@" "..." ; Remove-DnsServerResourceRecord -ComputerName "yourremote" -Force -ZoneName "example.com" -Name "@" -RRType "MX" -RecordData 5,"foo.com." ; echo CREATE "MX" "@" "..." ; Add-DnsServerResourceRecord -ComputerName "yourremote" -ZoneName "example.com" -Name "@" -TimeToLive $(New-TimeSpan -Seconds 0) -MX -MailExchange "foo2.com." -Preference 50`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
Loading…
Reference in a new issue