From 4efba2d92f3149d3620419f005e290e88824c0d0 Mon Sep 17 00:00:00 2001 From: Julien Le Goff Date: Thu, 23 Feb 2023 12:06:00 +0100 Subject: [PATCH 1/4] Updated and fixed tests using current PowerDNS version --- client.go | 3 +-- client_test.go | 6 +++--- docker-compose.yml | 22 ++-------------------- 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/client.go b/client.go index 72e315f..4580855 100644 --- a/client.go +++ b/client.go @@ -114,8 +114,7 @@ func removeRecords(rRSet zones.ResourceRecordSet, culls []libdns.Record) zones.R recs := rRSet.Records for i := len(recs) - 1; i >= 0; i-- { if recs[i].Content == item { - copy(recs[i:], recs[:i+1]) - recs = recs[:len(recs)-1] + recs = append(recs[:i], recs[i+1:]...) } } return recs diff --git a/client_test.go b/client_test.go index 44fb9bd..abd0af8 100644 --- a/client_test.go +++ b/client_test.go @@ -222,10 +222,10 @@ func TestPDNSClient(t *testing.T) { { Name: "2", Type: "A", - Value: "127.0.0.7", + Value: "127.0.0.5", }, }, - want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", "2:127.0.0.4", "2:127.0.0.5", "2:127.0.0.6"}, + want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", "2:127.0.0.4", "2:127.0.0.6", "2:127.0.0.7"}, }, { name: "Test Append and Add Zone", @@ -245,7 +245,7 @@ func TestPDNSClient(t *testing.T) { }, }, want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", - "2:127.0.0.4", "2:127.0.0.5", "2:127.0.0.6", "2:127.0.0.8", + "2:127.0.0.4", "2:127.0.0.6", "2:127.0.0.7", "2:127.0.0.8", "3:127.0.0.9"}, }, { diff --git a/docker-compose.yml b/docker-compose.yml index 5d1d8f5..70c2faf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,12 @@ version: "3" services: - mysql: - image: mariadb:10.6 - environment: - MYSQL_ROOT_PASSWORD: immelting - MYSQL_USER: powerdns - MYSQL_PASSWORD: secret - MYSQL_DATABASE: powerdns - volumes: - - database:/var/lib/mysql - networks: - - backend powerdns: - image: psitrax/powerdns - environment: - MYSQL_HOST: mysql - MYSQL_USER: powerdns - MYSQL_PASS: secret - MYSQL_DB: powerdns + image: powerdns/pdns-auth-47 networks: - backend volumes: - - ./.docker/pdns:/etc/pdns/conf.d + - ./.docker/pdns:/etc/powerdns/pdns.d ports: - 8081:8081 -volumes: - database: {} networks: backend: {} From 5d1c7e44847d8d0bc6effc073f1c0171a6239d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C4=8Cavoj?= Date: Mon, 21 Apr 2025 22:01:38 +0100 Subject: [PATCH 2/4] Update docker-compose to pdns 49 --- docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 70c2faf..75e9ccd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ -version: "3" services: powerdns: - image: powerdns/pdns-auth-47 + image: powerdns/pdns-auth-49 networks: - backend volumes: From 9b1da856987bc3eb9f51873d3fa2eff8d602f7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C4=8Cavoj?= Date: Mon, 21 Apr 2025 21:50:59 +0100 Subject: [PATCH 3/4] Adapt to libdns 1.0 interface --- client.go | 32 +++++++++++---------- client_test.go | 75 ++++++++++++++++++++++--------------------------- example/main.go | 7 ++--- go.mod | 2 +- go.sum | 10 ++----- provider.go | 28 +++++++++--------- 6 files changed, 71 insertions(+), 83 deletions(-) diff --git a/client.go b/client.go index 4580855..4ac141e 100644 --- a/client.go +++ b/client.go @@ -46,7 +46,7 @@ func (c *client) updateRRs(ctx context.Context, zoneID string, recs []zones.Reso return nil } -func mergeRRecs(fullZone *zones.Zone, records []libdns.Record) ([]zones.ResourceRecordSet, error) { +func mergeRRecs(fullZone *zones.Zone, records []libdns.RR) ([]zones.ResourceRecordSet, error) { // pdns doesn't really have an append functionality, so we have to fake it by // fetching existing rrsets for the zone and see if any already exist. If so, // merge those with the existing data. Otherwise just add the record. @@ -72,11 +72,11 @@ func mergeRRecs(fullZone *zones.Zone, records []libdns.Record) ([]zones.Resource } // now for our additions for _, rec := range recs { - if !dupes[rec.Value] { + if !dupes[rec.Data] { rr.Records = append(rr.Records, zones.Record{ - Content: rec.Value, + Content: rec.Data, }) - dupes[rec.Value] = true + dupes[rec.Data] = true } } rrsets = append(rrsets, rr) @@ -89,7 +89,7 @@ func mergeRRecs(fullZone *zones.Zone, records []libdns.Record) ([]zones.Resource } // generate RessourceRecordSets that will delete records from zone -func cullRRecs(fullZone *zones.Zone, records []libdns.Record) []zones.ResourceRecordSet { +func cullRRecs(fullZone *zones.Zone, records []libdns.RR) []zones.ResourceRecordSet { inHash := makeLDRecHash(records) var rRSets []zones.ResourceRecordSet for _, t := range fullZone.ResourceRecordSets { @@ -109,7 +109,7 @@ func cullRRecs(fullZone *zones.Zone, records []libdns.Record) []zones.ResourceRe } // remove culls from rRSet record values -func removeRecords(rRSet zones.ResourceRecordSet, culls []libdns.Record) zones.ResourceRecordSet { +func removeRecords(rRSet zones.ResourceRecordSet, culls []libdns.RR) zones.ResourceRecordSet { deleteItem := func(item string) []zones.Record { recs := rRSet.Records for i := len(recs) - 1; i >= 0; i-- { @@ -120,12 +120,12 @@ func removeRecords(rRSet zones.ResourceRecordSet, culls []libdns.Record) zones.R return recs } for _, c := range culls { - rRSet.Records = deleteItem(c.Value) + rRSet.Records = deleteItem(c.Data) } return rRSet } -func convertLDHash(inHash map[string][]libdns.Record) []zones.ResourceRecordSet { +func convertLDHash(inHash map[string][]libdns.RR) []zones.ResourceRecordSet { var rrsets []zones.ResourceRecordSet for _, recs := range inHash { if len(recs) == 0 { @@ -140,7 +140,7 @@ func convertLDHash(inHash map[string][]libdns.Record) []zones.ResourceRecordSet } for _, rec := range recs { rr.Records = append(rr.Records, zones.Record{ - Content: rec.Value, + Content: rec.Data, }) } rrsets = append(rrsets, rr) @@ -152,9 +152,9 @@ func key(Name, Type string) string { return Name + ":" + Type } -func makeLDRecHash(records []libdns.Record) map[string][]libdns.Record { +func makeLDRecHash(records []libdns.RR) map[string][]libdns.RR { // Keep track of records grouped by name + type - inHash := make(map[string][]libdns.Record) + inHash := make(map[string][]libdns.RR) for _, r := range records { k := key(r.Name, r.Type) @@ -196,9 +196,11 @@ func (c *client) zoneID(ctx context.Context, zoneName string) (string, error) { return shortZone.ID, nil } -func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.Record { - out := make([]libdns.Record, len(records)) - copy(out, records) +func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.RR { + out := make([]libdns.RR, len(records)) + for i, r := range records { + out[i] = r.RR() + } for i := range out { name := libdns.AbsoluteName(out[i].Name, zone) if !strings.HasSuffix(name, ".") { @@ -206,7 +208,7 @@ func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.Recor } out[i].Name = name if out[i].Type == "TXT" { - out[i].Value = txtsanitize.TXTSanitize(out[i].Value) + out[i].Data = txtsanitize.TXTSanitize(out[i].Data) } } return out diff --git a/client_test.go b/client_test.go index abd0af8..470f414 100644 --- a/client_test.go +++ b/client_test.go @@ -3,6 +3,7 @@ package powerdns import ( "context" "fmt" + "net/netip" "os" "os/exec" "reflect" @@ -135,10 +136,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.7", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.7"), }, }, want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", @@ -150,10 +150,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: "\"This is also some text\"", + libdns.TXT{ + Name: "1", + Text: "\"This is also some text\"", }, }, want: []string{ @@ -167,10 +166,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: "This is some weird text that isn't quoted", + libdns.TXT{ + Name: "1", + Text: "This is some weird text that isn't quoted", }, }, want: []string{ @@ -185,10 +183,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: `This is some weird text that "has embedded quoting"`, + libdns.TXT{ + Name: "1", + Text: `This is some weird text that "has embedded quoting"`, }, }, want: []string{`1:"This is text"`, `1:"This is also some text"`, @@ -201,10 +198,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: `ç is equal to \195\167`, + libdns.TXT{ + Name: "1", + Text: `ç is equal to \195\167`, }, }, want: []string{`1:"This is text"`, `1:"This is also some text"`, @@ -219,10 +215,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.5", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.5"), }, }, want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", "2:127.0.0.4", "2:127.0.0.6", "2:127.0.0.7"}, @@ -233,15 +228,13 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.8", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.8"), }, - { - Name: "3", - Type: "A", - Value: "127.0.0.9", + libdns.Address{ + Name: "3", + IP: netip.MustParseAddr("127.0.0.9"), }, }, want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", @@ -254,15 +247,13 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.1", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.1"), }, - { - Name: "1", - Type: "A", - Value: "127.0.0.1", + libdns.Address{ + Name: "1", + IP: netip.MustParseAddr("127.0.0.1"), }, }, want: []string{"1:127.0.0.1", "2:127.0.0.1", "3:127.0.0.9"}, @@ -294,10 +285,10 @@ func TestPDNSClient(t *testing.T) { } var have []string for _, rr := range recs { - if rr.Type != table.Type { + if rr.RR().Type != table.Type { continue } - have = append(have, fmt.Sprintf("%s:%s", rr.Name, rr.Value)) + have = append(have, fmt.Sprintf("%s:%s", rr.RR().Name, rr.RR().Data)) } sort.Strings(have) diff --git a/example/main.go b/example/main.go index 92cdd87..700894c 100644 --- a/example/main.go +++ b/example/main.go @@ -16,10 +16,9 @@ func main() { } _, err := p.AppendRecords(context.Background(), "example.org.", []libdns.Record{ - { - Name: "_acme_whatever", - Type: "TXT", - Value: "123456", + libdns.TXT{ + Name: "_acme_whatever", + Text: "123456", }, }) if err != nil { diff --git a/go.mod b/go.mod index 35444e0..7c4d62c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/libdns/powerdns go 1.16 require ( - github.com/libdns/libdns v0.2.2 + github.com/libdns/libdns v1.0.0-beta.1 github.com/mittwald/go-powerdns v0.6.6 ) diff --git a/go.sum b/go.sum index b13dc47..f949b10 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -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/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/mittwald/go-powerdns v0.5.2 h1:kfqr9ZNIuxOjjBaoJcOFiy/19VmKEUgfJPmObDglPJU= -github.com/mittwald/go-powerdns v0.5.2/go.mod h1:bI/sZBAWyTViDknOTp19VfDxVEnh1U7rWPx2aRKtlzg= -github.com/mittwald/go-powerdns v0.6.3 h1:t7HhWNosL+To+gJ+K4TRh1sZiFtB64ZrRlVvKPkpRGw= -github.com/mittwald/go-powerdns v0.6.3/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA= +github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= +github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/mittwald/go-powerdns v0.6.6 h1:yQcuszhl98+jJgELjD5ecfxCQWoshhnArexpwrwQxLY= github.com/mittwald/go-powerdns v0.6.6/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= diff --git a/provider.go b/provider.go index 885749d..ab96c01 100644 --- a/provider.go +++ b/provider.go @@ -19,19 +19,19 @@ type Provider struct { // ServerID is the id of the server. localhost will be used // if this is omitted. - ServerID string `json:"server_id,omitempty"` + ServerID string `json:"server_id,omitempty"` // APIToken is the auth token. - APIToken string `json:"api_token,omitempty"` + APIToken string `json:"api_token,omitempty"` // Debug - can set this to stdout or stderr to dump // debugging information about the API interaction with // powerdns. This will dump your auth token in plain text // so be careful. - Debug string `json:"debug,omitempty"` + Debug string `json:"debug,omitempty"` - mu sync.Mutex - c *client + mu sync.Mutex + c *client } // GetRecords lists all the records in the zone. @@ -47,14 +47,16 @@ func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record recs := make([]libdns.Record, 0, len(prec.ResourceRecordSets)) for _, rec := range prec.ResourceRecordSets { for _, v := range rec.Records { - recs = append(recs, libdns.Record{ - ID: prec.ID, - Type: rec.Type, - Name: libdns.RelativeName(rec.Name, zone), - Value: v.Content, - TTL: time.Second * time.Duration(rec.TTL), - Priority: 0, - }) + lrec, err := (libdns.RR{ + Type: rec.Type, + Name: libdns.RelativeName(rec.Name, zone), + Data: v.Content, + TTL: time.Second * time.Duration(rec.TTL), + }).Parse() + if err != nil { + return nil, err + } + recs = append(recs, lrec) } } return recs, nil From 39b8761a70db53400882849ce9953cecfec19d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C4=8Cavoj?= Date: Mon, 28 Apr 2025 20:57:13 +0100 Subject: [PATCH 4/4] Re-implement SVCB serialisation and enforce quotes around ech params pdns currently requires ech parameters to be quoted even if the contained value does not need quoting. Copy the SVCB serialisation logic from parent libdns and modify it to quote ech parameter values unconditionally. --- client.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++- example/main.go | 20 +++++++---- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index 4ac141e..2d69701 100644 --- a/client.go +++ b/client.go @@ -199,7 +199,12 @@ func (c *client) zoneID(ctx context.Context, zoneName string) (string, error) { func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.RR { out := make([]libdns.RR, len(records)) for i, r := range records { - out[i] = r.RR() + svcb, ok := r.(libdns.ServiceBinding) + if ok { + out[i] = svcbToRr(svcb) + } else { + out[i] = r.RR() + } } for i := range out { name := libdns.AbsoluteName(out[i].Name, zone) @@ -213,3 +218,84 @@ func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.RR { } return out } + +// This function is taken from libdns itself. +func svcbToRr(s libdns.ServiceBinding) libdns.RR { + var name string + var recType string + if s.Scheme == "https" || s.Scheme == "http" || s.Scheme == "wss" || s.Scheme == "ws" { + recType = "HTTPS" + name = s.Name + if s.URLSchemePort == 443 || s.URLSchemePort == 80 { + // Ok, we'll correct your mistake for you. + s.URLSchemePort = 0 + } + } else { + recType = "SVCB" + name = fmt.Sprintf("_%s.%s", s.Scheme, s.Name) + } + + if s.URLSchemePort != 0 { + name = fmt.Sprintf("_%d.%s", s.URLSchemePort, name) + } + + var params string + if s.Priority == 0 && len(s.Params) != 0 { + // The SvcParams should be empty in AliasMode, so we'll fix that for + // you. + params = "" + } else { + params = paramsToString(s.Params) + } + + return libdns.RR{ + Name: name, + TTL: s.TTL, + Type: recType, + Data: fmt.Sprintf("%d %s %s", s.Priority, s.Target, params), + } +} + +// This function is taken from libdns itself and modified to quote ECH params. +func paramsToString(params libdns.SvcParams) string { + var sb strings.Builder + for key, vals := range params { + if sb.Len() > 0 { + sb.WriteRune(' ') + } + sb.WriteString(key) + var hasVal, needsQuotes bool + if key == "ech" { + needsQuotes = true + } + for _, val := range vals { + if len(val) > 0 { + hasVal = true + } + if strings.ContainsAny(val, `" `) { + needsQuotes = true + } + if hasVal && needsQuotes { + break + } + } + if hasVal { + sb.WriteRune('=') + } + if needsQuotes { + sb.WriteRune('"') + } + for i, val := range vals { + if i > 0 { + sb.WriteRune(',') + } + val = strings.ReplaceAll(val, `"`, `\"`) + val = strings.ReplaceAll(val, `,`, `\,`) + sb.WriteString(val) + } + if needsQuotes { + sb.WriteRune('"') + } + } + return sb.String() +} diff --git a/example/main.go b/example/main.go index 700894c..af121ca 100644 --- a/example/main.go +++ b/example/main.go @@ -10,15 +10,21 @@ import ( func main() { p := &powerdns.Provider{ - ServerURL: "http://localhost", // required - ServerID: "localhost", // if left empty, defaults to localhost. - APIToken: "asdfasdfasdf", // required + ServerURL: "http://127.0.0.1:8081/", // required + ServerID: "localhost", // if left empty, defaults to localhost. + APIToken: "key", // required } - _, err := p.AppendRecords(context.Background(), "example.org.", []libdns.Record{ - libdns.TXT{ - Name: "_acme_whatever", - Text: "123456", + _, err := p.AppendRecords(context.Background(), "cavoj.net.", []libdns.Record{ + libdns.ServiceBinding{ + Name: "pdns-test", + Scheme: "https", + TTL: 0, + Priority: 1, + Target: "cavoj.net.", + Params: libdns.SvcParams{ + "ech": []string{"asdf"}, + }, }, }) if err != nil {