diff --git a/conversion.go b/conversion.go index b282cc3..e0744eb 100644 --- a/conversion.go +++ b/conversion.go @@ -2,23 +2,71 @@ package rfc2136 import ( "fmt" - "github.com/libdns/libdns" - "github.com/miekg/dns" + "reflect" "strings" "time" + + "github.com/libdns/libdns" + "github.com/miekg/dns" ) func recordToRR(rec libdns.Record, zone string) (dns.RR, error) { - str := fmt.Sprintf(`%s %d IN %s %s`, rec.Name, - int(rec.TTL.Seconds()), rec.Type, rec.Value) - zp := dns.NewZoneParser(strings.NewReader(str), zone, "") - rr, _ := zp.Next() + rrType := dns.StringToType[rec.Type] + rrConstructor := dns.TypeToRR[rrType] + var rr dns.RR + if rrConstructor == nil { + rr = new(dns.RFC3597) + } else { + rr = rrConstructor() + } + + // Create a zone file line representing the record. We're using reflection + // so that we can automatically support any new RR types that define these + // fields. + name := rec.Name + if name == "" { + name = "@" + } + + zoneLine := fmt.Sprintf("%s %d IN %s ", name, int(rec.TTL.Seconds()), rec.Type) + + priority := reflect.ValueOf(rr).Elem().FieldByName("Priority") + if !priority.IsValid() { + priority = reflect.ValueOf(rr).Elem().FieldByName("Preference") + } + + if priority.IsValid() { + zoneLine += fmt.Sprintf("%d ", rec.Priority) + } + + weight := reflect.ValueOf(rr).Elem().FieldByName("Weight") + if weight.IsValid() { + zoneLine += fmt.Sprintf("%d ", rec.Weight) + } + + target := reflect.ValueOf(rr).Elem().FieldByName("Target") + if target.IsValid() { + zoneLine += fmt.Sprintf("%s ", rec.Target) + } + + if rec.Value != "" { + zoneLine += rec.Value + } + zoneLine = strings.TrimSuffix(zoneLine, " ") + "\n" + + zp := dns.NewZoneParser(strings.NewReader(zoneLine), zone, "") + rr, _ = zp.Next() return rr, zp.Err() } func recordFromRR(rr dns.RR, zone string) libdns.Record { hdr := rr.Header() + rec := libdns.Record{ + Name: libdns.RelativeName(hdr.Name, zone), + TTL: time.Duration(hdr.Ttl) * time.Second, + } + // The record value is the full record string representation with the header string // prefix stripped. Package dns represents private-use and unknown records as // RFC3597 records. When those are formatted as string, their header prefix has a @@ -31,10 +79,38 @@ func recordFromRR(rr dns.RR, zone string) libdns.Record { typ = fmt.Sprintf("TYPE%d", hdr.Rrtype) hdrStr = fmt.Sprintf("%s\t%d\tCLASS%d\t%s\t", name, hdr.Ttl, hdr.Class, typ) } - return libdns.Record{ - Type: typ, - Name: libdns.RelativeName(hdr.Name, zone), - TTL: time.Duration(hdr.Ttl) * time.Second, - Value: strings.TrimPrefix(rr.String(), hdrStr), + + rec.Type = typ + + // Parse priority, weight, and target from the record value. We're using + // reflection so that we can automatically support any new RR types that + // define these fields. + priority := reflect.ValueOf(rr).Elem().FieldByName("Priority") + if !priority.IsValid() { + priority = reflect.ValueOf(rr).Elem().FieldByName("Preference") + } + + if priority.IsValid() { + priority := priority.Uint() + rec.Priority = uint(priority) + hdrStr += fmt.Sprintf("%d ", priority) + } + + weight := reflect.ValueOf(rr).Elem().FieldByName("Weight") + if weight.IsValid() { + weight := weight.Uint() + rec.Weight = uint(weight) + hdrStr += fmt.Sprintf("%d ", weight) + } + + target := reflect.ValueOf(rr).Elem().FieldByName("Target") + if target.IsValid() && typ != "SRV" { + rec.Target = target.String() + hdrStr += fmt.Sprintf("%s ", rec.Target) } + + // Get the value from the record string representation. + rec.Value = strings.TrimPrefix(rr.String(), hdrStr) + + return rec } diff --git a/conversion_test.go b/conversion_test.go index dbf4449..d664422 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -1,15 +1,21 @@ package rfc2136 import ( - "github.com/libdns/libdns" - "github.com/miekg/dns" + "encoding/base64" + "fmt" "net" "testing" "time" + + "github.com/libdns/libdns" + "github.com/miekg/dns" ) const zone = "example.com." +var echString = "AEj+DQBEAQAgACAdd+scUi0IYFsXnUIU7ko2Nd9+F8M26pAGZVpz/KrWPgAEAAEAAWQVZWNoLXNpdGVzLmV4YW1wbGUubmV0AAA=" +var echBytes, _ = base64.StdEncoding.DecodeString(echString) + var testCases = map[dns.RR]libdns.Record{ &dns.TXT{ Hdr: dns.RR_Header{ @@ -25,6 +31,7 @@ var testCases = map[dns.RR]libdns.Record{ Value: "\"hello world\"", TTL: 220 * time.Second, }, + &dns.TXT{ Hdr: dns.RR_Header{ Name: "txt.example.com.", @@ -84,6 +91,93 @@ var testCases = map[dns.RR]libdns.Record{ Value: `\# 5 0d10480001`, TTL: 150 * time.Second, }, + + &dns.HTTPS{ + SVCB: dns.SVCB{ + Hdr: dns.RR_Header{ + Name: "https.example.com.", + Rrtype: dns.TypeHTTPS, + Class: dns.ClassINET, + Ttl: 150, + }, + Priority: 2, + Target: "target.example.com.", + Value: []dns.SVCBKeyValue{ + &dns.SVCBAlpn{ + Alpn: []string{"h3", "h2"}, + }, + &dns.SVCBIPv4Hint{ + Hint: []net.IP{net.ParseIP("127.0.0.1")}, + }, + &dns.SVCBIPv6Hint{ + Hint: []net.IP{net.ParseIP("::1")}, + }, + &dns.SVCBECHConfig{ + ECH: echBytes, + }, + }, + }, + }: { + Type: "HTTPS", + Name: "https", + TTL: 150 * time.Second, + Priority: 2, + Target: "target.example.com.", + Value: fmt.Sprintf(`alpn="h3,h2" ipv4hint="127.0.0.1" ipv6hint="::1" ech="%s"`, echString), + }, + + &dns.MX{ + Hdr: dns.RR_Header{ + Name: "mx.example.com.", + Rrtype: dns.TypeMX, + Class: dns.ClassINET, + Ttl: 150, + }, + Preference: 10, + Mx: "mail.example.com.", + }: { + Type: "MX", + Name: "mx", + Value: "mail.example.com.", + TTL: 150 * time.Second, + Priority: 10, + }, + + &dns.SRV{ + Hdr: dns.RR_Header{ + Name: "srv.example.com.", + Rrtype: dns.TypeSRV, + Class: dns.ClassINET, + Ttl: 150, + }, + Priority: 10, + Weight: 20, + Port: 443, + Target: "service.example.com.", + }: { + Type: "SRV", + Name: "srv", + Value: "443 service.example.com.", + TTL: 150 * time.Second, + Priority: 10, + Weight: 20, + }, + + // Bare name + &dns.A{ + Hdr: dns.RR_Header{ + Name: "example.com.", + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 300, + }, + A: net.ParseIP("127.0.0.1"), + }: { + Type: "A", + Name: "", + Value: "127.0.0.1", + TTL: 300 * time.Second, + }, } func TestRecordFromRR(t *testing.T) { diff --git a/go.mod b/go.mod index 4877923..b2a075f 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/libdns/rfc2136 go 1.18 require ( - github.com/libdns/libdns v0.2.1 - github.com/miekg/dns v1.1.49 + github.com/libdns/libdns v0.2.3 + github.com/miekg/dns v1.1.63 ) require ( - golang.org/x/mod v0.4.2 // indirect - golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect - golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect - golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/tools v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 7f2d0ca..7f0c1dd 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,14 @@ -github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= -github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= -github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= -github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8= +github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= diff --git a/provider.go b/provider.go index 64f0ad9..40d57ed 100644 --- a/provider.go +++ b/provider.go @@ -3,9 +3,10 @@ package rfc2136 import ( "context" "fmt" + "time" + "github.com/libdns/libdns" "github.com/miekg/dns" - "time" ) type Provider struct { @@ -66,11 +67,35 @@ func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record return records, nil } -func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { - return p.SetRecords(ctx, zone, records) +func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { + zone = dns.Fqdn(zone) + + msg := dns.Msg{} + msg.SetUpdate(zone) + + rrs := make([]dns.RR, 0, len(records)) + for _, rec := range records { + rr, err := recordToRR(rec, zone) + if err != nil { + return nil, fmt.Errorf("invalid record %s: %w", rec.Name, err) + } + rrs = append(rrs, rr) + } + + msg.RemoveRRset(rrs) + msg.Insert(rrs) + + p.setTsig(&msg) + + _, _, err := p.client().ExchangeContext(ctx, &msg, p.Server) + if err != nil { + return nil, err + } + + return records, nil } -func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { +func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) { zone = dns.Fqdn(zone) msg := dns.Msg{}